From c959d83ceb1950520ecf99b90ba85fcac93b577f Mon Sep 17 00:00:00 2001 From: Madison Steiner Date: Sat, 13 Jun 2026 23:14:02 -0700 Subject: [PATCH 1/6] feat(plugin): native Claude Code plugin + dual-mode npx install - Add .claude-plugin/plugin.json (name=base, version=3.1.5 from package.json) - Add .claude-plugin/marketplace.json for claude plugin marketplace add - Copy src/commands -> commands/ with namespace-strip (base: prefix removed) - Copy src/skill/base.md -> skills/base/base.md - Copy src/framework -> base-framework/ (tasks, templates, context, etc.) - Copy src/hooks -> hooks/ with CLAUDE_PROJECT_DIR-first WORKSPACE_ROOT fix - Copy src/packages/base-mcp -> mcp/ with CLAUDE_PROJECT_DIR-first WORKSPACE_PATH fix - Wire hooks/hooks.json: 5 UserPromptSubmit + 1 SessionStart (apex-insights excluded) - Add .mcp.json registering base-mcp via node ${CLAUDE_PLUGIN_ROOT}/mcp/index.js - Rewrite @~/.claude/base-framework/ -> @${CLAUDE_PLUGIN_ROOT}/base-framework/ in commands/, skills/, base-framework/ plugin-native tree (66 refs) - src/ tree untouched: npx install reads src/ which retains @~/.claude/ refs - bin/install.js: add copyFileExpandingMacro() to expand ${CLAUDE_PLUGIN_ROOT} during any copy, ensuring no literal placeholder in npx-installed output Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 21 + .claude-plugin/plugin.json | 10 + .mcp.json | 11 + base-framework/context/base-principles.md | 69 +++ base-framework/frameworks/audit-strategies.md | 53 ++ .../frameworks/claude-config-alignment.md | 256 ++++++++ .../frameworks/claudemd-strategy.md | 158 +++++ .../frameworks/satellite-registration.md | 44 ++ base-framework/tasks/audit-claude-md.md | 171 ++++++ base-framework/tasks/audit-claude.md | 330 +++++++++++ base-framework/tasks/audit.md | 64 ++ base-framework/tasks/carl-hygiene.md | 142 +++++ base-framework/tasks/groom.md | 157 +++++ base-framework/tasks/history.md | 34 ++ base-framework/tasks/pulse.md | 83 +++ base-framework/tasks/scaffold.md | 389 +++++++++++++ base-framework/tasks/status.md | 35 ++ base-framework/tasks/surface-convert.md | 143 +++++ base-framework/tasks/surface-create.md | 184 ++++++ base-framework/tasks/surface-list.md | 42 ++ base-framework/tasks/weekly-domain-create.md | 173 ++++++ base-framework/tasks/weekly.md | 347 +++++++++++ base-framework/templates/claudemd-template.md | 102 ++++ base-framework/templates/workspace-json.md | 96 +++ base-framework/utils/scan-claude-dirs.py | 549 ++++++++++++++++++ bin/install.js | 46 +- commands/audit-claude-md.md | 44 ++ commands/audit-claude.md | 45 ++ commands/audit.md | 33 ++ commands/carl-hygiene.md | 33 ++ commands/groom.md | 35 ++ commands/history.md | 27 + commands/orientation.md | 87 +++ commands/orientation/tasks/deep-why.md | 132 +++++ commands/orientation/tasks/elevator-pitch.md | 115 ++++ commands/orientation/tasks/initiatives.md | 98 ++++ commands/orientation/tasks/key-values.md | 130 +++++ commands/orientation/tasks/new-orientation.md | 162 ++++++ commands/orientation/tasks/north-star.md | 97 ++++ commands/orientation/tasks/project-mapping.md | 103 ++++ commands/orientation/tasks/reorientation.md | 96 +++ commands/orientation/tasks/surface-vision.md | 113 ++++ commands/orientation/tasks/task-seeding.md | 93 +++ .../orientation/templates/operator-json.md | 88 +++ commands/pulse.md | 33 ++ commands/scaffold.md | 33 ++ commands/status.md | 28 + commands/surface-convert.md | 35 ++ commands/surface-create.md | 34 ++ commands/surface-list.md | 27 + commands/weekly-domain.md | 34 ++ commands/weekly.md | 39 ++ hooks/__pycache__/active-hook.cpython-314.pyc | Bin 0 -> 10313 bytes .../__pycache__/apex-insights.cpython-314.pyc | Bin 0 -> 11087 bytes .../__pycache__/backlog-hook.cpython-314.pyc | Bin 0 -> 5636 bytes .../base-pulse-check.cpython-314.pyc | Bin 0 -> 10538 bytes hooks/__pycache__/operator.cpython-314.pyc | Bin 0 -> 3189 bytes .../__pycache__/psmm-injector.cpython-314.pyc | Bin 0 -> 3662 bytes .../satellite-detection.cpython-314.pyc | Bin 0 -> 15549 bytes hooks/_template.py | 130 +++++ hooks/active-hook.py | 186 ++++++ hooks/apex-insights.py | 169 ++++++ hooks/backlog-hook.py | 123 ++++ hooks/base-pulse-check.py | 224 +++++++ hooks/hooks.json | 56 ++ hooks/operator.py | 61 ++ hooks/psmm-injector.py | 75 +++ hooks/satellite-detection.py | 328 +++++++++++ mcp/index.js | 120 ++++ mcp/package.json | 10 + mcp/tools/entities.js | 228 ++++++++ mcp/tools/operator.js | 106 ++++ mcp/tools/projects.js | 324 +++++++++++ mcp/tools/psmm.js | 206 +++++++ mcp/tools/satellite.js | 243 ++++++++ mcp/tools/state.js | 201 +++++++ mcp/tools/validate.js | 121 ++++ skills/base/base.md | 110 ++++ 78 files changed, 8520 insertions(+), 4 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .mcp.json create mode 100644 base-framework/context/base-principles.md create mode 100644 base-framework/frameworks/audit-strategies.md create mode 100644 base-framework/frameworks/claude-config-alignment.md create mode 100644 base-framework/frameworks/claudemd-strategy.md create mode 100644 base-framework/frameworks/satellite-registration.md create mode 100644 base-framework/tasks/audit-claude-md.md create mode 100644 base-framework/tasks/audit-claude.md create mode 100644 base-framework/tasks/audit.md create mode 100644 base-framework/tasks/carl-hygiene.md create mode 100644 base-framework/tasks/groom.md create mode 100644 base-framework/tasks/history.md create mode 100644 base-framework/tasks/pulse.md create mode 100644 base-framework/tasks/scaffold.md create mode 100644 base-framework/tasks/status.md create mode 100644 base-framework/tasks/surface-convert.md create mode 100644 base-framework/tasks/surface-create.md create mode 100644 base-framework/tasks/surface-list.md create mode 100644 base-framework/tasks/weekly-domain-create.md create mode 100644 base-framework/tasks/weekly.md create mode 100644 base-framework/templates/claudemd-template.md create mode 100644 base-framework/templates/workspace-json.md create mode 100644 base-framework/utils/scan-claude-dirs.py create mode 100644 commands/audit-claude-md.md create mode 100644 commands/audit-claude.md create mode 100644 commands/audit.md create mode 100644 commands/carl-hygiene.md create mode 100644 commands/groom.md create mode 100644 commands/history.md create mode 100644 commands/orientation.md create mode 100644 commands/orientation/tasks/deep-why.md create mode 100644 commands/orientation/tasks/elevator-pitch.md create mode 100644 commands/orientation/tasks/initiatives.md create mode 100644 commands/orientation/tasks/key-values.md create mode 100644 commands/orientation/tasks/new-orientation.md create mode 100644 commands/orientation/tasks/north-star.md create mode 100644 commands/orientation/tasks/project-mapping.md create mode 100644 commands/orientation/tasks/reorientation.md create mode 100644 commands/orientation/tasks/surface-vision.md create mode 100644 commands/orientation/tasks/task-seeding.md create mode 100644 commands/orientation/templates/operator-json.md create mode 100644 commands/pulse.md create mode 100644 commands/scaffold.md create mode 100644 commands/status.md create mode 100644 commands/surface-convert.md create mode 100644 commands/surface-create.md create mode 100644 commands/surface-list.md create mode 100644 commands/weekly-domain.md create mode 100644 commands/weekly.md create mode 100644 hooks/__pycache__/active-hook.cpython-314.pyc create mode 100644 hooks/__pycache__/apex-insights.cpython-314.pyc create mode 100644 hooks/__pycache__/backlog-hook.cpython-314.pyc create mode 100644 hooks/__pycache__/base-pulse-check.cpython-314.pyc create mode 100644 hooks/__pycache__/operator.cpython-314.pyc create mode 100644 hooks/__pycache__/psmm-injector.cpython-314.pyc create mode 100644 hooks/__pycache__/satellite-detection.cpython-314.pyc create mode 100644 hooks/_template.py create mode 100644 hooks/active-hook.py create mode 100644 hooks/apex-insights.py create mode 100644 hooks/backlog-hook.py create mode 100644 hooks/base-pulse-check.py create mode 100644 hooks/hooks.json create mode 100644 hooks/operator.py create mode 100644 hooks/psmm-injector.py create mode 100644 hooks/satellite-detection.py create mode 100644 mcp/index.js create mode 100644 mcp/package.json create mode 100644 mcp/tools/entities.js create mode 100644 mcp/tools/operator.js create mode 100644 mcp/tools/projects.js create mode 100644 mcp/tools/psmm.js create mode 100644 mcp/tools/satellite.js create mode 100644 mcp/tools/state.js create mode 100644 mcp/tools/validate.js create mode 100644 skills/base/base.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..9d66b49 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", + "name": "base", + "owner": { + "name": "ChristopherKahler", + "url": "https://github.com/ChristopherKahler" + }, + "metadata": { + "description": "BASE — Builder's Automated State Engine: workspace lifecycle management, data surfaces, and audit tooling for Claude Code." + }, + "plugins": [ + { + "name": "base", + "description": "BASE — Builder's Automated State Engine: workspace lifecycle management, data surfaces, and audit tooling for Claude Code.", + "version": "3.1.5", + "source": ".", + "category": "framework", + "keywords": ["base", "workspace", "scaffold", "audit", "groom", "surfaces", "operator", "lifecycle"] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..21a8485 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "base", + "version": "3.1.5", + "description": "BASE — Builder's Automated State Engine: workspace lifecycle management, data surfaces, and audit tooling for Claude Code.", + "author": { + "name": "Chris Kahler" + }, + "license": "MIT", + "homepage": "https://github.com/ChristopherKahler/base#readme" +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..ef001fd --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "base-mcp": { + "command": "node", + "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/index.js"], + "env": { + "CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}" + } + } + } +} diff --git a/base-framework/context/base-principles.md b/base-framework/context/base-principles.md new file mode 100644 index 0000000..a8f3e8f --- /dev/null +++ b/base-framework/context/base-principles.md @@ -0,0 +1,69 @@ +# BASE Principles + +## Core Laws + +1. **If it's not current, it's harmful.** Stale context documents feed AI bad information. Maintenance isn't optional. +2. **Every file earns its place.** If you can't explain why it's here in 5 seconds, it moves or dies. +3. **Archive > delete.** When in doubt, archive. You can always delete later. You can't un-delete. +4. **The workspace is the product.** Treat it like production code, not a scratch pad. +5. **Clean as you go.** The best time to file something correctly is when you create it. The second best time is now. +6. **Scaffold generates manifest. Manifest drives everything.** One configuration point. No manual bookkeeping. +7. **Tools register themselves.** PAUL projects auto-register with BASE. No human memory required. + +## Drift Score + +Drift is the gap between documented state and actual state. Measured in days-overdue across all tracked areas. + +- **0** — Everything current. Workspace is clean. +- **1-7** — Minor drift. Normal during execution sprints. Fix at next groom. +- **8-14** — Moderate drift. Context documents are likely misleading AI. Groom soon. +- **15+** — Critical drift. Sessions are operating on stale context. Groom NOW. + +## Maintenance Cadence + +| What | Default Cadence | Override | +|------|----------------|---------| +| projects.json | Every session or weekly | workspace.json | +| Project directory | Monthly | workspace.json | +| Tools/MCP | Monthly | workspace.json | +| System layer | Monthly | workspace.json | +| Full audit | Quarterly or after major shifts | On demand | + +## Backlog Rules + +Items have time-based properties enforced by grooming: + +- **Added** — auto-set when item enters backlog +- **Review-by** — priority-based: High=7d, Medium=14d, Low=30d +- **Staleness** — 2x review-by threshold. Auto-archive if reached without action. + +During groom: items past review-by surface as "decide or kill." Items past staleness auto-archive with a note. + +## Graduation Flow + +Backlog items don't sit forever. They graduate to active when the operator is ready to work on them. + +``` +BACKLOG (status=backlog in projects.json) + → ACTIVE (status updated to in_progress/todo via base_update_project) + → DONE (archived via base_archive_project with outcome) +``` + +**TASKS vs PROJECTS:** A task is bounded — it has a finish line. "Extract .mcp.json secrets" is a task. "Build the CARL MCP server" might start as a task but could become a project if it grows. The operator decides during groom. + +**Graduation is never automatic.** The groom flow asks explicitly: "Ready to work on any backlog items?" The operator decides what graduates and where it lands. + +**Items can also move backward:** An active project that loses priority can return to backlog status. A project that stalls can move to DEFERRED. Nothing is permanent. + +## Scaffold Modes + +BASE scaffold operates in two modes: + +- **Standard** (`/base:scaffold`) — Data layer only. Creates `.base/` with workspace.json, `.base/data/state.json`, ROADMAP.md. Scans and tracks what exists. Framework-agnostic. +- **Full** (`/base:scaffold --full`) — Data layer + projects.json + entities.json. Offers CLAUDE.md audit. The "batteries included" version for AI builders who want the full system. + +Standard mode works for any workspace. Full mode provides Chris's proven operational structure. + +## File Location + +BASE operates strictly out of `.base/`. All data (projects.json, entities.json, state.json, psmm.json) lives in `.base/data/`. Configuration (workspace.json) and documentation (ROADMAP.md) live in `.base/`. Data is accessed via MCP tools (`base_list_projects`, `base_add_project`, `base_update_project`, `base_get_state`, etc.). `.base/data/` is the canonical location for all structured data. diff --git a/base-framework/frameworks/audit-strategies.md b/base-framework/frameworks/audit-strategies.md new file mode 100644 index 0000000..fb3eaa9 --- /dev/null +++ b/base-framework/frameworks/audit-strategies.md @@ -0,0 +1,53 @@ +# Audit Strategies + +Reusable audit strategies that can be applied to any workspace area. The workspace manifest (`workspace.json`) maps areas to strategies. The audit command reads the manifest and applies the appropriate strategy to each area. + +## Strategies + +### staleness +**Applies to:** Data files (projects.json, state.json, any tracked document) +**What it does:** Check file modification timestamps against configured thresholds. Flag files past their groom cadence. +**Config:** +- `threshold_days` — days after which the file is considered stale +**Output:** List of stale files with age, recommended action (update or review) + +### classify +**Applies to:** Directories with lifecycle items (projects/, clients/) +**What it does:** List all items in the directory. For each, present to operator for classification: active, archive, or delete. Check for planning docs, recent activity, git history. +**Config:** +- `states` — classification options (default: ["active", "archive", "delete"]) +- `archive_path` — where archived items go (default: `{path}/_archive/`) +**Output:** Classification decisions, items moved to archive, items deleted + +### cross-reference +**Applies to:** Tools/servers that have a config file mapping (e.g., MCP servers vs .mcp.json) +**What it does:** Compare directory contents against a configuration file. Identify directories not referenced in config (orphaned) and config entries pointing to missing directories (broken). +**Config:** +- `config_file` — path to the configuration file to cross-reference +**Output:** Orphaned items, broken references, recommendations + +### dead-code +**Applies to:** System directories (hooks, commands, skills) +**What it does:** Scan for files that appear unused — no references from other files, no recent invocations, no clear purpose. Presents findings for human decision. +**Config:** +- `reference_check` — whether to search for references in other files (default: true) +**Output:** Potentially dead files with evidence, operator decides keep/delete + +### pipeline-status +**Applies to:** Content pipelines, task queues, any workflow with stages +**What it does:** Check items in each pipeline stage. Flag stuck items (in same stage too long), empty stages, bottlenecks. +**Config:** +- `stages` — ordered list of pipeline stages +- `stuck_threshold_days` — days in one stage before flagging +**Output:** Pipeline health report, stuck items, stage distribution + +## Extending Strategies + +Custom strategies can be added for workspace-specific needs. A strategy is defined by: +1. A name (kebab-case) +2. What it applies to (description) +3. What it checks (logic) +4. What config it needs (parameters) +5. What it outputs (findings format) + +Add custom strategies to this file and reference them in `workspace.json`. diff --git a/base-framework/frameworks/claude-config-alignment.md b/base-framework/frameworks/claude-config-alignment.md new file mode 100644 index 0000000..17eb927 --- /dev/null +++ b/base-framework/frameworks/claude-config-alignment.md @@ -0,0 +1,256 @@ +# Claude Config Alignment Strategy + +Standalone strategy for auditing `.claude/` directory sprawl across a workspace. Discovers all `.claude/` directories, catalogs their contents, classifies each item against a global/workspace/project hierarchy, and produces a remediation plan. + +Designed to be composed into any audit workflow that touches the system layer. The `/base:audit-claude` command references this strategy directly. The general `/base:audit` can compose it in when running system-layer checks. + +--- + +## When to Use + +- During initial BASE setup on an existing workspace (lots of legacy `.claude/` dirs) +- As part of periodic workspace audits +- After installing a new tool/skill globally and wanting to clean up project-level copies +- When the user suspects their Claude Code config is fragmented + +--- + +## Discovery Rules + +### What to scan +- All directories named `.claude` under the workspace root +- Recursively (projects may nest: `apps/foo/bar/.claude/`) + +### What to skip +- `node_modules/` — third-party packages may contain `.claude/` dirs +- `_archive/` — archived projects are frozen, don't audit +- `.git/` — git internals +- `vendor/`, `dist/`, `build/` — build artifacts + +### What to catalog per directory +For each discovered `.claude/` directory, record: +- **Path** (relative to workspace root) +- **hooks/** — list filenames +- **commands/** — list subdirectories and files +- **skills/** — list skill directories +- **rules/** — list files +- **settings.json** — exists? contents summary +- **settings.local.json** — exists? contents summary +- **Other files** — anything unexpected + +--- + +## Git Boundary Awareness + +Understanding git boundaries is critical for correct classification. The scanner dataset includes `git_boundary` data for each directory. + +### How git boundaries affect visibility + +- **`has_own_git: true`** — This project has its own git root. It does NOT see the workspace root `.claude/` or workspace `.mcp.json`. It only sees global `~/.claude/` + its own `.claude/`. +- **`has_own_git: false`** — This project inherits the workspace root. It sees global `~/.claude/` + workspace root `.claude/` + its own `.claude/`. + +### What this changes about classification + +If a project has its own git root and contains a hook that also exists in the workspace root `.claude/`, that's **not a duplicate running twice** — the workspace root version is invisible to that project. Removing the local copy would leave the project with NO version of that hook. + +For own-git projects, the right framing is: +- Does a current version exist in **global** `~/.claude/`? If yes, the local copy is a true DUPLICATE (global is always visible). +- Does a version only exist in **workspace root** `.claude/`? Then the local copy is the project's ONLY access to that functionality. The right recommendation is PROMOTE_TO_GLOBAL — centralize it so all projects benefit, then clean up local copies. +- Is the local copy an outdated version of something in a baseline? It's DIVERGED. + +--- + +## Classification Rules + +Each item found in a project-level `.claude/` must be classified into exactly one category. These rules define how. + +**Classification order:** TEMPLATE → ACCIDENTAL → DUPLICATE → DIVERGED → PROMOTE_TO_GLOBAL → STALE → GLOBAL_CANDIDATE → PROJECT_SPECIFIC + +### DUPLICATE — Exists in a visible baseline, safe to remove + +An item is a DUPLICATE if: +- Its MD5 hash matches a file in a baseline the project can **actually see** +- Global baseline (`~/.claude/`): always checked — global is always visible +- Workspace root baseline (`.claude/`): only checked if `has_own_git: false` +- A match against a **non-visible** baseline (workspace root for own-git projects) is NOT a duplicate — see PROMOTE_TO_GLOBAL +- Common examples: hooks copied into project dirs that now run globally, skills that were installed globally after being copied locally + +**Verification before removal:** +1. Confirm the global version is the current/active version (not the other way around) +2. Confirm the project's `settings.json` doesn't reference the local copy with a relative path that would break if removed +3. If the local copy has modifications not in the global version, flag as DIVERGED instead + +### DIVERGED — Local copy differs from global + +An item is DIVERGED if: +- A version exists both locally and globally +- The local copy has meaningful differences (not just whitespace or path variations) +- Requires human decision: merge local changes into global? Keep local override? Or discard local changes? + +**Never auto-resolve diverged items. Always present both versions and ask.** + +### PROMOTE_TO_GLOBAL — Centralize to global, then clean up copies + +An item is PROMOTE_TO_GLOBAL if: +- It exists in workspace root `.claude/` (or the same pattern appears across multiple own-git projects) but NOT in global `~/.claude/` +- For own-git projects: a file matching a non-visible baseline (workspace root) is NOT a duplicate — it's a signal something should be centralized +- When multiple projects have the same hook with the same MD5, that's strong evidence it belongs in global +- This is the most valuable finding: "put this in global and stop copying it into every project" + +**PROMOTE_TO_GLOBAL is about removing the need for copies.** Once promoted, all project copies become true DUPLICATES (global is always visible) and can be safely removed. + +**Promotion is a suggestion, never automatic.** After promoting to global, the global `settings.json` must also register the hook or it won't fire. + +### GLOBAL_CANDIDATE — Should be promoted to global (single occurrence) + +An item is a GLOBAL_CANDIDATE if: +- It exists in a project-level `.claude/` but NOT in global +- It serves a user-level purpose (not project-specific) +- It would be useful across multiple projects +- Examples: a custom skill the user installed in one project but uses everywhere, a hook that provides general utility + +**Promotion is a suggestion, never automatic.** Present the item, explain why it's a candidate, let operator decide. + +### PROJECT_SPECIFIC — Legitimately belongs here + +An item is PROJECT_SPECIFIC if: +- It references project-local paths, configs, or conventions +- It only makes sense in the context of this specific project +- Examples: project-specific commands, MCP server lists tailored to that project's stack, hooks that interact with project-local files + +**These stay. Note them for reference but take no action.** + +### STALE — References things that no longer exist + +An item is STALE if: +- settings.json references MCP servers that aren't in the current `.mcp.json` or global config +- hooks reference scripts or tools that have been renamed or removed +- Settings use old configuration patterns that Claude Code no longer supports +- The `.claude/` directory hasn't been modified in 60+ days AND the project itself shows no recent activity + +**Present specific evidence of staleness. Never assume — prove it.** + +### ACCIDENTAL — Clearly unintentional + +An item is ACCIDENTAL if: +- Nested `.claude/.claude/` directories +- Empty `.claude/` directories (no files at all) +- `.claude/` inside directories that aren't projects (temp dirs, scratch folders) + +**Safe to remove, but still confirm with operator.** + +### TEMPLATE — Intentional scaffold template + +An item is TEMPLATE if: +- It lives in a directory named `_template/`, `template/`, or `templates/` +- It contains placeholder values (e.g., `{{PROJECT_NAME}}`) +- It's designed to be copied, not used directly + +**Never modify templates. Note them and move on.** + +--- + +## Settings Reconciliation + +Project-level `settings.json` and `settings.local.json` require special handling because they override global settings. + +### Check for +1. **Hook definitions that duplicate global hooks** — If global `settings.json` already runs `base-pulse-check.py` on UserPromptSubmit, a project that also defines it runs it twice (or runs a stale version) +2. **Stale MCP server references** — Server names change. Old lists reference servers that no longer exist +3. **Empty hook arrays** — `"UserPromptSubmit": []` overrides global hooks with nothing, potentially breaking the user's setup +4. **Permission overrides** — Project-level allow/deny that may conflict with or duplicate global permissions +5. **enabledMcpjsonServers** — Lists that reference old server names + +### Critical safety rule +**An empty hooks array `[]` in a project settings.json OVERRIDES the global hooks with nothing.** This is the most dangerous pattern — it silently disables all hooks for that project. Always flag this explicitly. + +--- + +## Remediation Safety Protocol + +This is the most important section. `.claude/` configuration is what makes Claude Code work. A broken config means a broken development environment. Every remediation action must follow these rules: + +### Before any change +1. **Explain what will change and why** — No "cleaning up your config." Say exactly: "Removing `apps/casegate-v2/.claude/hooks/carl-hook.py` because an identical version runs globally from `~/.claude/hooks/dynamic-rules-loader.py`. The global hook already fires on every prompt in every project." +2. **Show evidence** — Side-by-side comparison, file dates, path references +3. **Wait for explicit approval** — Not "I'll go ahead and clean these up." Ask: "Approve this removal? [y/n]" + +### During changes +4. **One category at a time** — Process all ACCIDENTAL items first (lowest risk), then DUPLICATES, then STALE, then DIVERGED. Save GLOBAL_CANDIDATE promotions for last. +5. **Verify after each change** — After removing a hook, confirm the project's settings.json no longer references it. After removing a skill, confirm no commands reference it. +6. **Never delete settings.json itself** — Even if everything in it is stale. The file's existence may matter. Clean its contents instead, or flag for operator to remove manually. + +### After all changes +7. **Summary report** — What was changed, what was kept, what needs manual follow-up +8. **Recommend a test** — "Open Claude Code in {project} and verify hooks fire correctly" + +### What this workflow NEVER does +- Modify `~/.claude/` (global config) without explicit promotion approval +- Delete a `.claude/` directory entirely (may have gitignore or settings implications) +- Batch-delete without per-item confirmation +- Assume a "messy" config is wrong — it may be working exactly as intended +- Move files between directories (copy + verify + then remove original) + +--- + +## Output Format + +The discovery phase produces an inventory. Present it as: + +``` +## .claude/ Directory Inventory + +Found {N} .claude/ directories ({N} excluding templates and root). + +### {relative/path/.claude/} + hooks/: carl-hook.py, get-current-time-cst.py + settings.json: yes (hooks: 2 UserPromptSubmit) + settings.local.json: yes (MCP servers: 12) + skills/: ui-ux-pro-max/ + commands/: (none) + Last modified: 2026-02-26 + + Classification: + - hooks/carl-hook.py → DUPLICATE (identical to ~/.claude/hooks/dynamic-rules-loader.py) + - hooks/get-current-time-cst.py → DUPLICATE (identical to ~/.claude/hooks/get-current-time-cst.sh) + - settings.json hooks → STALE (references local hook paths that would be removed) + - settings.local.json → PROJECT_SPECIFIC (MCP server list is project-tailored) + - skills/ui-ux-pro-max/ → DUPLICATE (exists at ~/.claude/skills/ui-ux-pro-max/) +``` + +The remediation phase groups by action type and risk level: + +``` +## Remediation Plan + +### Safe Removals (ACCIDENTAL) +1. apps/hunter-exotics/.claude/.claude/ — nested .claude dir (accidental) + Action: Delete entire nested directory + Risk: None + +### Duplicate Removals +2. apps/casegate-v2/.claude/hooks/carl-hook.py — identical to global + Action: Delete file + Risk: Low — must also clean settings.json hook reference + +(... etc, one per item ...) + +### Requires Decision (DIVERGED) +5. apps/hunter-exotics/.claude/settings.local.json — has project-specific MCP list + Local version: [12 servers including project-specific ones] + Global version: [different set] + Recommendation: Keep as PROJECT_SPECIFIC +``` + +--- + +## Composability + +This strategy is a standalone reference document. It does not modify or depend on other strategy files. Any workflow can compose it in: + +- `/base:audit-claude` — reads this strategy directly as its framework +- `/base:audit` — can reference this when auditing the system-layer area +- `/base:groom` — can optionally flag `.claude/` drift during system-layer groom step +- Manual invocation — an operator can ask "audit my .claude dirs" and Claude reads this file + +No registration in workspace.json is required. No modification to audit-strategies.md is needed. diff --git a/base-framework/frameworks/claudemd-strategy.md b/base-framework/frameworks/claudemd-strategy.md new file mode 100644 index 0000000..ca6e363 --- /dev/null +++ b/base-framework/frameworks/claudemd-strategy.md @@ -0,0 +1,158 @@ +# The CLAUDE.md Strategy + +Composable framework for auditing and writing high-performance CLAUDE.md files. Source of truth for the `/base:audit-claude-md` workflow. + +--- + +## The Structure: What, Why, Who, Where, How + +Every CLAUDE.md follows five sections in this exact order. Each section answers one question. Together they give Claude complete operating context without bloat. + +### What +What this document is and what this workspace contains. + +One line. Sets the contract. Claude knows this is its instruction set. + +> "This file provides guidance to Claude Code when working with code in this repository." + +### Why +The philosophy. Identity context. Why this workspace exists. + +This is where you separate identity from operations. CLAUDE.md answers the "who am I working with?" question. Operational details (how to run a specific project, current sprint status) live elsewhere and get referenced with `@` pointers. + +### Who +Business context. Who the user is, what they do, what matters to them. + +Not a life story. Enough context that Claude can make relevant suggestions. Business name, what the business does, revenue model, team, tech stack. The goal: Claude should be able to answer "what does this person's business look like?" after reading this section. + +### Where +Workspace structure. The directory map. + +This section serves double duty: +1. Tells Claude where to find things +2. Plants the blueprint for the workspace architecture — the Where section describes a structure that may or may not exist yet + +Use a tree diagram. Include: +- Each top-level directory and its purpose +- Key subdirectories if they have meaning +- What goes where (decision guide) + +### How +Ecosystem strategy. Tool strategy. Git strategy. Quick references. + +This is the operational layer: +- What systems/frameworks are in use (compact table with location pointers) +- Git strategy (what gets tracked, what gets ignored) +- Rules (see NEVER pattern below) +- Quick reference table for common actions + +--- + +## The NEVER Pattern: Rules as Anti-Patterns + +**The single most important discovery from 40+ sessions of compliance testing.** + +### The Pattern + +``` +NEVER [wrong action] — [right action] +``` + +Negative framing with absolute language gets near-perfect adherence. Every high-compliance rule follows this format. + +### Why This Works + +It's a binary check. Claude can look at its own output and ask: "Did I do the forbidden thing or not?" No judgment call. No interpretation. No sliding scale. + +| Framing | Compliance | Why | +|---------|-----------|-----| +| "Try to use templates when possible" | ~40% | Ambiguous. "When possible" is a judgment call Claude resolves toward skipping. | +| "Always use templates" | ~65% | Better, but "always" gets weighed against context. Claude may decide exceptions apply. | +| "NEVER create from scratch — use templates" | ~95% | Binary. The forbidden action is unambiguous. | + +### Rules for Writing Rules + +1. **One rule per line.** No compound rules. If it has "and" in it, split it. +2. **The wrong action comes first.** Claude anchors on the first thing it reads. Make the forbidden action the anchor. +3. **The right action is the alternative.** Not a lecture — a redirect. "Don't do X — do Y instead." +4. **Defer complexity to reference docs.** The rule stays short. The details live in a separate file loaded on demand. + +--- + +## The @ Reference System + +CLAUDE.md stays lean by pointing to other files instead of inlining their content. + +``` +@LINKS.md — Personal branding URLs +@projects/dashboard-build/PLANNING.md — Active project context +``` + +Claude reads `@`-referenced files on demand. Your CLAUDE.md stays lean while still giving Claude access to deep context. + +**What to inline vs what to reference:** +- **Inline:** Identity (who, what, why), workspace structure, rules, quick references +- **Reference:** Active project details, task lists, state files, detailed specs + +--- + +## What Stays Out + +The test: **if it changes every week, it doesn't belong in CLAUDE.md.** + +CLAUDE.md is the constitution, not the daily newspaper. + +| Doesn't belong | Where it goes | +|----------------|---------------| +| Task lists / current work | State files, project tracking systems | +| Project specs | Each project's PLANNING.md | +| Rules for specific domains | Domain-specific rule files loaded on demand (e.g., CARL) | +| Daily status updates | State files | +| Detailed framework docs | Separate files, `@`-referenced | + +--- + +## Line Budget + +Target: **under 100 lines.** This is a routing document, not a knowledge base. + +If your CLAUDE.md is over 100 lines, content is being inlined that should be referenced or removed. Common offenders: +- Inline system/framework descriptions (replace with a compact table) +- Redundant location tables when a tree diagram already exists +- Documentation system descriptions (the tree covers this) +- Standalone sections for single rules (consolidate into Rules section) + +--- + +## Audit Criteria + +When auditing an existing CLAUDE.md, check: + +### Structure +- [ ] Follows What → Why → Who → Where → How order +- [ ] Each section is correctly labeled +- [ ] No orphan sections outside the five-section model + +### Content Placement +- [ ] Identity/philosophy in Why (not scattered) +- [ ] Business context in Who (not bloated) +- [ ] Directory map in Where (tree format, not just a table) +- [ ] All operational content in How (git, systems, rules, quick ref) +- [ ] No task lists, state files, or volatile data inlined + +### Rules +- [ ] All rules use NEVER pattern (not "always", "try to", "prefer") +- [ ] One rule per line, no compound rules +- [ ] Wrong action first, right action as redirect +- [ ] Complex rules defer to reference docs + +### Leanness +- [ ] Under 100 lines (excluding code blocks in Where tree) +- [ ] No redundant sections (e.g., key locations table + tree diagram) +- [ ] System descriptions are a compact table, not inline paragraphs +- [ ] `@` references used for anything that changes frequently + +### CARL Integration (if present) +- [ ] Operational rules that belong in domain-specific contexts are flagged for CARL migration +- [ ] CLAUDE.md rules are constitutional (identity-level), not operational +- [ ] No duplication between CLAUDE.md rules and CARL domain rules diff --git a/base-framework/frameworks/satellite-registration.md b/base-framework/frameworks/satellite-registration.md new file mode 100644 index 0000000..486fd6b --- /dev/null +++ b/base-framework/frameworks/satellite-registration.md @@ -0,0 +1,44 @@ +# Satellite Registration Framework + +## What Are Satellites + +Satellites are projects that live in their own git repos inside the workspace (e.g., `apps/*`). They run their own Claude Code sessions independently. BASE needs visibility into them without owning them. + +## Registration Flow + +### Automatic (via PAUL init) +When `/paul:init` runs in a subdirectory: +1. Check if parent directory has `.base/workspace.json` +2. If yes, write registration entry: project name, path, engine type, state file path, date +3. Report: "Registered with BASE workspace: {workspace-name}" + +### Automatic (via BASE scaffold) +When `/base:scaffold` runs: +1. Scan configured satellite directories (default: `apps/`) +2. Detect existing `.paul/` directories +3. Auto-register discovered projects +4. Report: "Found {N} satellite projects. Registered." + +### Automatic (via BASE groom) +During groom: +1. Read registered satellites from workspace.json +2. Check each path exists (clean up broken registrations) +3. Scan satellite directories for unregistered projects with `.paul/` +4. Flag: "Found unregistered project: {name}. Register?" + +## Health Checks + +During `/base:pulse` and `/base:groom`, for each satellite: +1. Read the state file (e.g., `.paul/STATE.md`) +2. Check last modification date +3. Extract current phase/milestone if parseable +4. Report health: active, stale, or unknown + +BASE never modifies satellite state. It only reads and reports. PAUL (or whatever engine) manages the project. BASE manages the workspace those projects live in. + +## Deregistration + +Satellites are deregistered when: +- The project directory no longer exists (auto-cleaned during groom) +- The user explicitly removes it during audit +- The project is archived (moved to `_archive/` or similar) diff --git a/base-framework/tasks/audit-claude-md.md b/base-framework/tasks/audit-claude-md.md new file mode 100644 index 0000000..8cac3c0 --- /dev/null +++ b/base-framework/tasks/audit-claude-md.md @@ -0,0 +1,171 @@ + +Audit an existing CLAUDE.md against the CLAUDE.md Strategy framework, then interactively rewrite it with user approval at each stage. Detects CARL installation and routes operational rules accordingly. + + + +As an AI builder, I want my CLAUDE.md audited against a proven strategy so I get a compliant, lean configuration file — with operational rules properly routed to CARL or preserved as an artifact for later. + + + +- During /base:scaffold (optional step) +- When user says "audit my claude.md", "improve my claude.md", "rewrite my claude.md" +- Entry point: /base:audit-claude-md + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/claudemd-strategy.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/claudemd-template.md + + + + + +Load the CLAUDE.md Strategy framework and template. + +1. Read `@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/claudemd-strategy.md` — this is the source of truth +2. Read `@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/claudemd-template.md` — this is the structural reference +3. Internalize: five-section model (What/Why/Who/Where/How), NEVER pattern, line budget, audit criteria + +You MUST understand the full strategy before reading the user's file. The strategy defines what "correct" looks like. + + + +Read the user's existing CLAUDE.md and catalog every piece of content. + +1. Read `CLAUDE.md` from workspace root +2. If no CLAUDE.md exists → skip to `generate_fresh` step +3. For every section, paragraph, rule, table, and reference in the file, classify each as: + - **KEEP** — belongs in CLAUDE.md per the strategy (identity, structure, constitutional rules) + - **REMOVE** — doesn't belong (volatile data, task lists, state references, redundant sections) + - **RESTRUCTURE** — right content, wrong location or format (e.g., rule using "always" instead of NEVER pattern, operational content in wrong section) + - **CARL_CANDIDATE** — operational rule or domain-specific behavior that belongs in a rules engine, not CLAUDE.md + +4. Count total lines. Note if over 100-line budget. + + + +Present the full audit to the user. This is INTERACTIVE — do not proceed without approval. + +Present a structured report: + +**Section Order Compliance:** +- Current order vs required order (What → Why → Who → Where → How) +- Orphan sections (content outside the five-section model) + +**Content Classification:** +For each piece of existing content, show: +``` +[KEEP] "Business context section" → stays in Who +[REMOVE] "Active Work section" → volatile, belongs in state management +[RESTRUCTURE] "LSP rule" → move to How/Rules, convert to NEVER pattern +[CARL_CANDIDATE] "When writing tests, always..." → operational rule, not identity +``` + +**Line Budget:** +- Current: {N} lines +- Target: under 100 +- Reduction plan: what removal/restructuring achieves + +**Missing Content:** +- Sections required by strategy that don't exist yet + +Ask: **"Does this audit look right? Any items you want to reclassify before I proceed?"** + +Wait for user response. Adjust classifications based on their feedback. + + + +Check for CARL installation to determine rule routing. + +1. Check for `.carl/manifest` in workspace root (workspace-level CARL) +2. Check for `~/.carl/manifest` (global-level CARL) +3. If CARL found: + - Report: "CARL detected at {location}. Operational rules will be proposed as CARL domain rules." + - Note which existing CARL domains overlap with CARL_CANDIDATE items +4. If CARL not found: + - Report: "No CARL installation detected." + - Offer: "I can: (a) install CARL now and set up domains, or (b) save operational rules as an artifact in `.base/artifacts/` for later CARL setup" + - If user picks (b): rules go to `.base/artifacts/claudemd-audit-rules.md` (create `.base/artifacts/` if needed) + - If user picks (a): guide CARL installation, then route rules to domains + +Wait for user decision before proceeding. + + + +Build the new CLAUDE.md section by section, presenting each for approval. + +For EACH section (What, Why, Who, Where, How): + +1. **Show the proposed content** for that section +2. **Show what changed** vs the original (additions, removals, restructuring) +3. **Ask for approval**: "Accept this section? Or modify?" +4. If user modifies → incorporate changes +5. If user accepts → lock section, move to next + +**Section-specific guidance:** + +**What:** One-liner. Rarely needs changes unless missing entirely. + +**Why:** Philosophy/identity. Pull from existing philosophy content. Strip operational routing details that belong in How. + +**Who:** Business context. Preserve existing content. Trim if bloated. Ensure it answers "what does this person's business look like?" + +**Where:** Scan actual filesystem with `ls` to verify tree accuracy. Update tree to match reality. Remove subdirectories that don't add meaning. Collapse verbose trees to stay within budget. + +**How:** Assemble from: +- Systems table (compact, one row per system) +- Git strategy table (from existing or detected .gitignore) +- Rules (NEVER pattern only — constitutional rules stay here, operational rules route to CARL/artifact) +- Quick reference (common actions → instructions) + + + +Handle operational rules that were classified as CARL_CANDIDATE. + +**If CARL is installed:** +1. Group candidates by likely CARL domain (DEVELOPMENT, CONTENT, CLIENTS, etc.) +2. Present: "These rules are proposed for CARL domain `{domain}`:" +3. Show each rule in NEVER pattern format +4. Ask: "Approve these for CARL? Modify? Skip?" +5. For approved rules: provide the exact CARL command to add them (do NOT execute without explicit permission) + +**If CARL artifact path:** +1. Write all CARL_CANDIDATE rules to `.base/artifacts/claudemd-audit-rules.md` +2. Format: group by proposed domain, NEVER pattern, include rationale +3. Tell user: "Rules saved to `.base/artifacts/claudemd-audit-rules.md`. When you're ready to set up CARL, run this file through Claude to create the domains." + + + +Write the approved CLAUDE.md. + +1. Assemble all approved sections into final document +2. Verify line count (warn if over 100) +3. Write to `CLAUDE.base.md` in workspace root (NEVER overwrite CLAUDE.md directly) +4. Present final diff summary: sections added, removed, restructured, rules routed + +Tell user: +- "Review `CLAUDE.base.md`. To adopt it: `mv CLAUDE.base.md CLAUDE.md`" +- "Your original CLAUDE.md is untouched." +- If CARL candidates were routed: "Operational rules are in {location}." + + + + + +- `CLAUDE.base.md` — strategy-compliant CLAUDE.md ready for adoption +- CARL domain rules (if CARL installed) or `.base/artifacts/claudemd-audit-rules.md` (if not) +- Original CLAUDE.md untouched + + + +- [ ] Strategy framework loaded and understood before audit begins +- [ ] Every line of existing CLAUDE.md classified (KEEP/REMOVE/RESTRUCTURE/CARL_CANDIDATE) +- [ ] Full audit presented to user with approval gate before rewriting +- [ ] CARL installation detected and rule routing decided with user +- [ ] Each section proposed individually with user approval +- [ ] All rules use NEVER pattern +- [ ] Final output under 100 lines +- [ ] Operational rules routed to CARL or saved as artifact +- [ ] Original CLAUDE.md never modified +- [ ] User informed of how to adopt and next steps + diff --git a/base-framework/tasks/audit-claude.md b/base-framework/tasks/audit-claude.md new file mode 100644 index 0000000..9015ef0 --- /dev/null +++ b/base-framework/tasks/audit-claude.md @@ -0,0 +1,330 @@ + +Audit all .claude/ directories across a workspace. Discover sprawl, classify each item against the global/workspace/project hierarchy, plan remediation with operator approval at every step, and execute changes safely. + + + +As an AI builder with multiple projects in my workspace, I want all my .claude/ directories audited for duplication, staleness, and misplacement, so that my Claude Code configuration is clean, consistent, and I know exactly what's project-specific vs what should be global. + + + +- During BASE setup on an existing workspace with legacy projects +- Periodically as part of workspace optimization +- After installing global skills/hooks and wanting to clean up project copies +- When user says "audit my claude config", "clean up my .claude dirs", "check claude setup" +- Entry point routes here via /base:audit-claude + + + +@frameworks/claude-config-alignment.md + + + +ALL audit findings MUST be written to a markdown report file at `.base/audits/claude-config-{YYYY-MM-DD}.md`. + +Do NOT dump findings into the chat as inline text. The chat is for brief status updates, questions, and confirmations only. The report is where all detail lives. + +The report must be: +- Written in clean markdown with tables, headers, and clear visual hierarchy +- Readable by a human who opens it in any markdown viewer +- Comprehensive: current state, classifications with evidence, remediation plan with risk levels, items kept and why + +After writing the report, tell the operator: "Audit report written to `.base/audits/claude-config-{date}.md`. Review it, then tell me which remediation groups to execute." + +During remediation execution, update the report with results (append a "Remediation Results" section). + + + + + +Run the scanner utility to produce a complete, verified dataset. + +This step uses a deterministic Python script that scans the entire workspace and produces structured JSON. The script handles all data collection — baselines, directory discovery, file hashing, settings parsing. Claude does NOT gather this data manually. + +**Run the scanner:** +``` +python3 ~/.claude/base-framework/utils/scan-claude-dirs.py --workspace {workspace_root} +``` + +The script outputs a JSON file to `.base/audits/data-sets/claude-scan-{date}.json` containing: +- **Baselines:** Complete inventory of global `~/.claude/` and workspace root `.claude/` (every file with MD5 hash) +- **MCP registry:** All server names from `.mcp.json` +- **Directories:** Every project-level `.claude/` directory with full contents (hooks, commands, skills, rules, settings — all with MD5 hashes) +- **Summary:** Counts of hooks, commands, skills, settings files, nested dirs, empty dirs, templates + +**Read the JSON output.** This is your single source of truth for all subsequent steps. Do not make ad-hoc bash calls to re-discover or re-hash files. If it's not in the scan data, re-run the scanner. + +If the scanner fails, diagnose and fix before proceeding. Do not fall back to manual scanning. + + + +Classify every item in every project-level .claude/ directory. + +**CRITICAL: Git boundary awareness.** + +The scanner dataset includes `git_boundary` data for each directory. This tells you what each project actually sees when Claude Code boots there: + +- `has_own_git: true` → This project has its own git root. It does NOT see the workspace root `.claude/` or workspace `.mcp.json`. It only sees global `~/.claude/` + its own `.claude/`. +- `has_own_git: false` → This project inherits the workspace root. It sees global `~/.claude/` + workspace root `.claude/` + its own `.claude/`. + +**This changes what "duplicate" means.** If a project has its own git root and contains a hook that also exists in the workspace root `.claude/`, that's not a duplicate running twice — the workspace root version is invisible to that project. Removing the local copy would leave the project with NO version of that hook. + +For own-git projects, the right framing is: +- Does a current version exist in **global** `~/.claude/`? If yes, the local copy is a true duplicate (global is always visible). +- Does a version only exist in **workspace root** `.claude/`? Then the local copy is the project's ONLY access to that functionality. The right recommendation is to **promote to global** so all projects benefit, THEN clean up local copies. +- Is the local copy an outdated version of something in a baseline? It's DIVERGED — recommend updating to current or promoting current to global. + +**Classification order:** + +1. **TEMPLATE** — In a template directory? Mark and skip. +2. **ACCIDENTAL** — Nested .claude dirs, empty dirs. +3. **DUPLICATE** — MD5 matches a baseline the project can ACTUALLY SEE: + - Global baseline: always checked (always visible) + - Workspace root baseline: only checked if `has_own_git: false` + - Match against visible baseline → DUPLICATE (safe to remove) + - Match against non-visible baseline → NOT a duplicate. See PROMOTE_TO_GLOBAL. +4. **DIVERGED** — Same-named file exists in a visible baseline but MD5 differs. +5. **PROMOTE_TO_GLOBAL** — Item exists in workspace root `.claude/` (or same pattern across multiple projects) but NOT in global `~/.claude/`. These are hooks/skills/commands the user wants everywhere but hasn't centralized. This is the most valuable finding — "put this in global and stop copying it into every project." When multiple projects have the same hook, that's strong evidence. +6. **STALE** — References things that no longer exist: + - settings.json hooks pointing to missing files + - settings.local.json MCP servers not in registry (also check if MCP is even visible per git boundary) + - Files untouched for 60+ days in an inactive project +7. **GLOBAL_CANDIDATE** — Exists only in one project, serves a user-level purpose, not in any baseline. +8. **PROJECT_SPECIFIC** — Everything else that legitimately belongs in the project. + +**Classification rules:** +- A file is DUPLICATE only if its MD5 matches a baseline the project can actually see +- A file matching a non-visible baseline is a signal that something should be promoted to global +- PROMOTE_TO_GLOBAL is the key recommendation for own-git projects with hooks/skills that mirror workspace root +- When multiple projects share the same hook, that's strong evidence it belongs in global +- When in doubt, check the hash (hashes don't lie) and the git_boundary data (assumptions lie) + + + +Analyze settings.json and settings.local.json files specifically. + +These are the most dangerous files because they control Claude Code behavior: + +1. For each project-level settings.json: + a. Parse hook definitions — list every hook command + b. For each hook, check: does this reference a local file? Does that file exist? Is it a duplicate? + c. Check if hook arrays are empty `[]` — this OVERRIDES global hooks with nothing + d. Compare hook list against global settings.json hooks — identify double-execution patterns + e. Check permissions — do they conflict with or duplicate global? + +2. For each project-level settings.local.json: + a. List every entry in enabledMcpjsonServers + b. Check each server name against MCP baseline — flag any that don't exist + c. Note enableAllProjectMcpServers boolean + +3. **Git-aware analysis** — For each project, use git_boundary data to determine: + - Which hooks are ACTUALLY running (global + project? or global + workspace root + project?) + - Is the project's `.mcp.json` visibility correct? (own-git projects can't see workspace .mcp.json) + - Would removing a local hook leave the project with NO version of that functionality? + +4. Build a "Settings Danger Report" section: + - Which projects have duplicate hooks running (only possible if project inherits workspace root) + - Which own-git projects rely on local hooks as their ONLY source of CARL/time/etc functionality + - Which settings.json files will have dangling references after hook files are removed + - Which settings.local.json files have stale MCP entries (or reference MCP servers they can't even see) + + + +Self-audit: verify every classification is correct before writing the report. + +This step exists because classification errors are the most damaging mistake this audit can make. A misclassified DUPLICATE that's actually PROJECT_SPECIFIC means deleting something the user needs. + +**Verification checks:** + +1. **DUPLICATE verification** — For every item classified as DUPLICATE: + - Confirm the baseline file it supposedly duplicates actually exists + - Confirm the MD5 hashes actually match (re-check, don't trust prior step) + - Confirm the baseline version is the current/active version + +2. **GLOBAL_CANDIDATE verification** — For every item classified as GLOBAL_CANDIDATE: + - Search global baseline for any file with the same name (case-insensitive) + - Search workspace root baseline for any file with the same name + - Search for the same MD5 hash across all baselines (catches renamed copies) + - If found anywhere → reclassify as DUPLICATE + +3. **DIVERGED verification** — For every item classified as DIVERGED: + - Confirm both versions actually exist and differ + - Note which is newer (by file modification date) + +4. **STALE verification** — For every STALE settings entry: + - Confirm the referenced resource actually doesn't exist (not just renamed) + - Check both .mcp.json AND global settings for MCP servers + +5. **Completeness check** — Count total items classified vs total items discovered. If they don't match, something was missed. Find and classify the missing items. + +6. **Cross-reference check** — For items in the remediation plan that depend on each other (e.g., deleting a hook file + cleaning the settings.json that references it), verify both sides of the dependency are in the plan. + +If any reclassifications happen in this step, update all downstream plan entries. + + + +Write the complete audit report to `.base/audits/claude-config-{YYYY-MM-DD}.md`. + +Report structure: +1. **MD5 disclaimer** — Always include this at the top, right after the metadata block, as a blockquote: + > *\* This audit uses MD5 fingerprinting to classify files. An MD5 hash is a unique fingerprint generated from a file's contents — if two files produce the same fingerprint, they are byte-for-byte identical. If the fingerprints differ, the files are different, even if they share the same name. This means every "DUPLICATE" classification in this report is provably exact, and every "DIVERGED" classification is provably different — not guessed, not assumed.* +2. **Summary** — What's wrong, what's fine, item counts by classification +2. **Baselines** — Brief description of what exists globally and at workspace root (so the reader understands what "duplicate of" means) +3. **Findings by Directory** — Each project .claude/ gets its own section with: + - Table of items: item path, classification, evidence (MD5 match, baseline reference) + - Last modified date + - Risk notes specific to that directory +4. **Settings Reconciliation** — Dangerous patterns: duplicate hook execution, stale MCP refs, empty hook overrides +5. **Remediation Plan** — Grouped by risk level (1-6), each item has: + - Item number + - Exact path + - Action (delete, clean, keep, promote) + - Why (with specific evidence) + - Dependencies (e.g., "after removing hook file, settings.json needs cleanup in Group 4") +6. **Items Kept (No Action)** — What's staying and exactly why +7. **Next Steps** — What the operator should do + +Tell the operator: "Audit report written to `.base/audits/claude-config-{date}.md`. Review it, then we'll decide how to handle remediation." + +**Wait for operator to review the report before proceeding to graduation routing.** + + + +Route remediation into a structured execution path. Present the operator with options. + +**Display routing prompt:** +``` +════════════════════════════════════════ +AUDIT COMPLETE — REMEDIATION ROUTING +════════════════════════════════════════ + +The audit report is ready at .base/audits/claude-config-{date}.md + +How would you like to handle remediation? + +[1] Create standalone PAUL project + → Initializes a new PAUL project seeded with audit findings + → Best for: large remediation, multiple phases, traceability needed + +[2] Add milestone to existing PAUL project + → Adds a remediation milestone to a registered satellite + → Best for: audit is part of ongoing workspace optimization work + +[3] Execute ad-hoc (legacy) + → Proceed with group-by-group remediation in this session + → Best for: small, straightforward cleanups + +════════════════════════════════════════ +``` + +**If option 1 (standalone PAUL project):** + +1. Ask: "Where should the project be created? (e.g., `projects/claude-audit-remediation`)" +2. If user has no obvious location, suggest `projects/` as a convention +3. Provide instructions: + ``` + To proceed: + 1. mkdir -p {path} + 2. cd {path} + 3. Run /paul:init + 4. When defining scope, reference the audit report: + @.base/audits/claude-config-{date}.md + + The audit report's Remediation Plan section maps directly + to PAUL phases — each remediation group can be a phase. + ``` +4. Do NOT auto-run /paul:init — the operator invokes it in the right context +5. Exit this workflow (remediation happens through PAUL) + +**If option 2 (add milestone to existing PAUL project):** + +1. Read `.base/workspace.json` for registered satellites: + ``` + Read workspace.json → satellites array → list projects with paths and current status + ``` +2. Present registered projects: + ``` + Registered PAUL projects: + [a] apps/base — v2.4 Config Governance (in progress) + [b] apps/casegate-v2 — v1.0 (if registered) + ... + + Select a project, or provide a path: + ``` +3. After selection, provide instructions: + ``` + To proceed: + 1. In the selected project, run /paul:milestone + 2. When defining scope, reference the audit report: + @.base/audits/claude-config-{date}.md + + The audit report's Remediation Plan section provides + the scope — each remediation group maps to a phase. + ``` +4. Do NOT auto-run /paul:milestone +5. Exit this workflow (remediation happens through PAUL) + +**If option 3 (ad-hoc / legacy):** + +Proceed directly to the execute_remediation step below. This preserves the original workflow behavior for operators who prefer immediate execution. + +``` +Proceeding with ad-hoc remediation. +Tell me which groups to execute (or "approve all"). +``` + + + +Execute approved remediation items one group at a time. + +For each approved group: +1. Announce in chat: "Executing Group {N}: {description} ({count} items)" +2. For each item: + a. Execute the change + b. If the change involves a settings.json modification, verify the JSON is still valid after edit + c. Brief confirmation in chat: "{path} — done" +3. After each group completes: + a. Verify no broken references were created + b. Report in chat: "Group {N} complete. {count} items processed." +4. If any item fails or produces an unexpected result, STOP and report to operator + +**Between groups, pause and confirm: "Group {N} complete. Proceed to Group {N+1}?"** + + + +Verify the workspace is healthy after all remediation and update the report. + +1. Re-scan every `.claude/` directory that was modified +2. For each modified directory verify: + - settings.json is valid JSON (if it was modified) + - No hook arrays reference files that don't exist + - No empty `.claude/` directories left behind (unless intentional) + - No orphaned subdirectories (hooks/ dir with no hooks in it) +3. Run a quick re-discovery scan to catch anything the remediation might have exposed +4. Append a "Remediation Results" section to the audit report file: + - What was executed (by group) + - What was verified + - Any issues found during verification + - Projects the operator should test by opening Claude Code in them + +Tell the operator: "Remediation complete. Report updated. Recommend testing Claude Code in: {list of modified projects}." + + + + + +Complete .claude/ directory audit with inventory, classification, verified remediation, and post-remediation verification — all in a structured markdown report. + + + +- [ ] Three baselines built (global, workspace root, MCP registry) before any classification +- [ ] Every file hashed with MD5 — no classification without hash evidence +- [ ] Every item classified against ALL baselines, not just one +- [ ] Self-audit pass completed — all classifications verified, no GLOBAL_CANDIDATE that's actually a DUPLICATE +- [ ] Item count verified: classified items == discovered items (nothing missed) +- [ ] Settings files analyzed for dangerous patterns (empty hooks, stale MCP refs, double execution) +- [ ] Report written to .base/audits/ as structured markdown (not inline chat) +- [ ] Operator reviewed report and approved remediation before execution +- [ ] Changes executed one group at a time with verification between groups +- [ ] Post-remediation scan confirms no broken references or invalid JSON +- [ ] Report updated with remediation results + diff --git a/base-framework/tasks/audit.md b/base-framework/tasks/audit.md new file mode 100644 index 0000000..e015cb8 --- /dev/null +++ b/base-framework/tasks/audit.md @@ -0,0 +1,64 @@ + +Deep workspace optimization. Dynamically generate audit phases from the workspace manifest, run each area's configured audit strategy, and execute operator-approved changes. + + + +As an AI builder, I want a thorough workspace audit that adapts to my workspace structure, so that every area gets properly reviewed regardless of how complex my setup is. + + + +- Quarterly or after major workspace shifts +- When user says "base audit", "deep clean", "optimize workspace" +- Entry point routes here via /base:audit + + + + + +Read workspace manifest and generate audit phases dynamically. + +1. Read `.base/workspace.json` +2. For each area, create an audit phase using its configured strategy +3. Present phase list: "Audit will cover {N} phases: {list with strategies}" +4. Create task tracking for each phase + +**Wait for operator confirmation. Allow them to skip or reorder phases.** + + + +Run each phase using its configured audit strategy. + +For each phase: +1. Announce: "Phase {N}: {area-name} ({strategy})" +2. Execute the strategy (reference frameworks/audit-strategies.md) +3. Present findings +4. Collect operator decisions (keep/archive/delete/move) +5. Execute approved changes +6. Mark phase complete + +Strategies are documented in `@frameworks/audit-strategies.md`. + + + +Record the audit results. + +1. Update `.base/data/state.json` +2. Write audit record to `.base/audits/{YYYY-MM-DD}.md` +3. Log to `.base/ROADMAP.md` +4. Report final summary: phases completed, items changed, new drift score + + + + + +Complete workspace audit with dynamic phases. All areas reviewed, changes executed, audit recorded. + + + +- [ ] Phases generated dynamically from manifest (not hardcoded) +- [ ] Each area audited using its configured strategy +- [ ] Operator approved all changes before execution +- [ ] Audit record written to audits/ directory +- [ ] state.json updated +- [ ] ROADMAP.md updated with audit entry + diff --git a/base-framework/tasks/carl-hygiene.md b/base-framework/tasks/carl-hygiene.md new file mode 100644 index 0000000..a7864f7 --- /dev/null +++ b/base-framework/tasks/carl-hygiene.md @@ -0,0 +1,142 @@ + +Structured CARL domain maintenance. Review staged proposals, flag stale rules, audit domain health, and keep CARL lean and accurate. All operations target carl.json as the single source of truth. + + + +As an AI builder, I want a guided CARL maintenance session, so that my domain rules stay relevant, staged proposals get decided on, and CARL doesn't become a dumping ground of stale rules. + + + +- Monthly (on configured cadence) +- When pulse reports overdue CARL hygiene +- When user says "carl hygiene", "review carl rules", "clean up carl" +- Entry point routes here via /base:carl-hygiene + + + + + +Gather CARL health data and present summary. + +1. Read `.base/workspace.json` for `carl_hygiene` config (threshold, max rules, last run) +2. Use `carl_v2_list_domains` to get all domains with rule/decision counts and state +3. Use `carl_v2_get_staged` to check for pending staging proposals +4. For each active domain from `carl_v2_list_domains`: + - Note rule count and decision count + - Use `carl_v2_get_domain(domain)` to inspect rule `last_reviewed` fields + - Flag rules where `last_reviewed` is null or older than `staleness_threshold_days` + - Flag domains exceeding `max_rules_per_domain` +5. Present summary: + ``` + CARL Hygiene Assessment + ━━━━━━━━━━━━━━━━━━━━━━ + Staged proposals: {N} pending + Domains: {N} total ({N} active, {N} inactive), {N} with stale rules, {N} over max + Total rules: {N} across all domains + Total decisions: {N} ({N} active, {N} archived) + Last hygiene: {date or "never"} + ``` + +**Wait for operator confirmation before proceeding.** + + + +Process each pending staged proposal. + +Use `carl_v2_get_staged` to retrieve all proposals. For each with `status: "pending"`: +1. Present: + ``` + Proposal {id} — {proposed_domain} + Proposed: {created_at} | Source: {source_session or "manual"} + Rule: "{rule_text}" + Rationale: {rationale} + ``` +2. Ask: "**Approve**, **Kill**, or **Defer**?" +3. Execute: + - Approve → `carl_v2_approve_proposal(id)` — promotes to domain rule with `source: "staging"`, removes from staging + - Kill → Read `.carl/carl.json`, remove the proposal entry from the `staging` array, write back + - Defer → skip (stays pending for next hygiene) + +If no pending proposals: "No staged proposals to review." and move to next step. + +Process one proposal at a time. Wait for response between each. + + + +Review rules flagged as stale (last_reviewed is null or older than threshold). + +For each domain with stale rules (identified in assess step): +1. Present domain name and total rule count +2. For each stale rule: + ``` + [{DOMAIN}] Rule {id} — last reviewed {date or "never"} ({days} days ago) + "{text}" + ``` +3. Ask: "**Keep** (update reviewed date), or **Kill**?" +4. Execute: + - Keep → Use `carl_v2_replace_rules(domain, rules)` with updated `last_reviewed` set to today's date for kept rules + - Kill → Use `carl_v2_remove_rule(domain, rule_id)` (with "Are you sure?" confirmation) + +If no stale rules: "All rules are current. No staleness issues." and move to next step. + +Process one domain at a time. + + + +Quick domain health check — guided Q&A. + +1. List all domains (active and inactive) with rule counts from `carl_v2_list_domains` +2. For each active domain: + - "Do the recall phrases for **{domain}** still match how you talk about this work?" + - Show current recall keywords for reference +3. Check for domains over `max_rules_per_domain`: + - "Domain **{X}** has {N} rules (max: {max}). Any candidates to kill or consolidate?" +4. Check for inactive domains: "These domains are inactive: {list}. Reactivate or remove any?" +5. Ask: "Any new domains to create? Any to deactivate?" + +**Guided Q&A — don't force changes, just surface questions.** + + + +Quick check on per-domain decision health. + +For each domain that has decisions (from `carl_v2_get_domain`): +1. List decisions with date and status +2. Flag decisions older than 90 days (might be outdated) +3. Flag domains with 0 decisions that might benefit from decision logging +4. Ask: "Any decisions to archive?" → use `carl_v2_archive_decision(id)` if yes + +**Brief pass — decisions are mostly self-maintaining.** + + + +Record the hygiene session. + +1. Update `.base/workspace.json` → `carl_hygiene.last_run` to today's date +2. Update `.base/data/state.json` → note CARL hygiene completed with timestamp +3. Report: + ``` + CARL Hygiene Complete + ━━━━━━━━━━━━━━━━━━━━━ + Proposals: {N} processed ({N} approved, {N} killed, {N} deferred) + Rules reviewed: {N} ({N} kept, {N} killed) + Decisions reviewed: {N} ({N} archived) + Domains: {N} active, {N} inactive + Next hygiene due: {date based on cadence} + ``` + + + + + +CARL domains reviewed and maintained. Staged proposals decided. Stale rules addressed. Domain health verified. Hygiene session logged to workspace.json. + + + +- [ ] All pending proposals presented and decided (approve/kill/defer) +- [ ] Stale rules flagged and reviewed with operator +- [ ] Domain health check completed (rule counts, recall phrases) +- [ ] workspace.json carl_hygiene.last_run updated +- [ ] state.json updated with hygiene completion +- [ ] Operator confirmed completion of each step + diff --git a/base-framework/tasks/groom.md b/base-framework/tasks/groom.md new file mode 100644 index 0000000..b343a98 --- /dev/null +++ b/base-framework/tasks/groom.md @@ -0,0 +1,157 @@ + +Structured weekly maintenance cycle. Walk through each workspace area, review with operator, enforce backlog time-based rules, graduate ready items, and log the groom. + + + +As an AI builder, I want a guided workspace maintenance session, so that my context documents stay current, my backlog items graduate when ready, and my workspace doesn't drift. + + + +- Weekly (on configured groom day) +- When pulse reports overdue grooming +- When user says "base groom", "let's groom", "workspace maintenance" +- Entry point routes here via /base:groom + + + + + +Determine what needs grooming. + +1. Read `.base/workspace.json` manifest +2. Use `base_get_state` MCP tool (or read `.base/data/state.json`) for last groom dates per area +3. Identify which areas are due for grooming (past their cadence) +4. Sort by staleness (most overdue first) +5. Present: "Groom session starting. {N} areas due for review: {list}. Estimated time: {N*5} minutes." + +**Wait for operator confirmation before proceeding.** + + + +Review projects — the working memory for all active, blocked, and backlog work. + +**Data source:** `base_list_projects` MCP tool (reads projects.json) + +1. Use `base_list_projects` to pull all projects grouped by status +2. Present summary: "{N} active, {N} blocked, {N} backlog, last updated {date}" +3. For each active/blocked project: "Still active? Status changed? Next action current?" +4. For each task (type=task): "Done? Still in progress? Blocked?" +5. Archive completed items via `base_archive_project` +6. Ask: "Anything new to add?" +7. Updates via `base_update_project` + +**Backlog items (status=backlog) — enforce time-based rules:** +1. For each backlog item, check `created_at` or `review_by` against thresholds: + - High priority: 7 days + - Medium priority: 14 days + - Low priority: 30 days +2. Items past review-by → surface: "These items need a decision: {list}" +3. Items past staleness (2x review-by) → "Auto-archiving: {list} (past {N} days without action)" +4. Process operator decisions on each flagged item + +**Graduation check:** +5. For each remaining backlog item, ask: "Ready to work on any of these?" +6. If yes — update status from `backlog` to `in_progress` or `todo` via `base_update_project` +7. If no — keep with updated review-by date + +**The graduation question is explicit every groom.** Items don't graduate silently — the operator decides. + +Voice-friendly: walk through one entry at a time, wait for response. + + + +Review directory-type areas (projects/, clients/, tools/). + +For each directory area due for grooming: +1. List contents +2. Flag anything that looks orphaned or new since last groom +3. Ask: "Anything to archive, delete, or reclassify?" +4. Execute approved changes + + + +Review PAUL satellite project health. + +1. Read `.base/workspace.json` — collect all satellite entries where `groom_check: true` +2. If no satellites have `groom_check: true` → skip this step silently +3. For each eligible satellite: + a. Read its STATE.md at the path in `satellite.state` (relative to workspace root) + b. If STATE.md is missing or unreadable → note as "⚠️ {name}: STATE.md not found" + c. Get last activity timestamp: + - PRIMARY: read `satellite.last_activity` from workspace.json entry (ISO timestamp written by session-start hook from paul.json) + - FALLBACK: if `last_activity` not present in workspace.json, parse "Last activity" line from the satellite's STATE.md + - If neither available → note as "⚠️ {name}: cannot determine last activity" + d. Parse "Loop Position" section from STATE.md → extract PLAN/APPLY/UNIFY markers (✓ = done, ○ = pending) + e. Evaluate health criteria: + - **STUCK LOOP**: Loop shows PLAN ✓ APPLY ○ or PLAN ✓ APPLY ✓ UNIFY ○, AND last activity > 7 days ago + - **ABANDONED PHASE**: Last activity > 14 days ago AND milestone status is not COMPLETE + - **MILESTONE DRIFT**: Milestone marked COMPLETE, loop shows ○ ○ ○ (no new milestone started), AND last activity > 14 days ago +4. Collect all issues across satellites +5. If issues found: surface as: + ``` + ⚠️ Satellite health issues: + - {satellite-name}: {issue type} (last active: {date}) + ``` +6. If no issues: output single line "Satellites: all healthy ({N} checked)" + +**Report only — do NOT auto-fix.** Operator decides what to do with flagged satellites. + + + +Review system layer areas (hooks, commands, skills, CARL). + +1. Quick scan for obvious dead items +2. Only flag if something clearly wrong +3. Ask: "Any system changes to note?" +4. If CARL hygiene is enabled (workspace.json `carl_hygiene.proactive: true`): + - Use `carl_v2_get_staged` to check for pending proposals in carl.json + - Use `carl_v2_list_domains` to check rule counts and spot-check `last_reviewed` dates for staleness + - Surface: "{N} staged proposals, {N} stale rules — run /base:carl-hygiene?" + + + +Record the groom session. + +1. Use `base_record_groom` MCP tool to update state.json (sets last_groom, advances next_groom_due) +2. Use `base_update_drift` MCP tool to reset drift indicators +3. Update area timestamps via `base_update_area` for each groomed area +4. Write groom summary to `.base/grooming/{YYYY}-W{NN}.md`: + ```markdown + # Groom Summary — Week {NN}, {YYYY} + + **Date:** {YYYY-MM-DD} + **Areas Reviewed:** {list} + **Drift Score:** {before} → 0 + + ## Changes + - {what changed} + + ## Graduated from Backlog + - {item} → project (status: in_progress) + + ## Archived / Killed + - {item} (reason) + + ## Next Groom Due + {YYYY-MM-DD} + ``` +5. Report: "Groom complete. Drift score: 0. Next groom due: {date}." + + + + + +Updated workspace state. All due areas reviewed and current. Backlog time-based rules enforced. Ready items graduated. Groom summary logged. + + + +- [ ] All overdue areas reviewed with operator +- [ ] Projects updated via base_update_project / base_archive_project +- [ ] Backlog time-based rules enforced (review-by, staleness) +- [ ] Graduation question asked explicitly for backlog items +- [ ] Graduated items updated from backlog → active status +- [ ] state.json updated via base_record_groom +- [ ] Groom summary written to grooming/ directory +- [ ] Drift score reset to 0 +- [ ] Operator confirmed completion of each area + diff --git a/base-framework/tasks/history.md b/base-framework/tasks/history.md new file mode 100644 index 0000000..e8edefe --- /dev/null +++ b/base-framework/tasks/history.md @@ -0,0 +1,34 @@ + +Show workspace evolution over time. Read ROADMAP.md and present the chronological record of major workspace changes. + + + +As an AI builder, I want to see how my workspace has evolved, so that I can understand the trajectory and make informed decisions about future changes. + + + +- When user wants to review workspace history +- Entry point routes here via /base:history + + + + + +Read and present workspace evolution. + +1. Read `.base/ROADMAP.md` +2. Present chronologically: dates, what changed, why +3. Include audit summaries and major groom outcomes +4. If ROADMAP.md is empty or missing: "No history yet. Run /base:audit or /base:groom to start building your workspace timeline." + + + + + +Chronological workspace evolution timeline from ROADMAP.md. + + + +- [ ] History presented in clear chronological format +- [ ] Includes both audits and significant groom outcomes + diff --git a/base-framework/tasks/pulse.md b/base-framework/tasks/pulse.md new file mode 100644 index 0000000..2b6dae7 --- /dev/null +++ b/base-framework/tasks/pulse.md @@ -0,0 +1,83 @@ + +Daily workspace activation. Read workspace state, calculate drift, present health dashboard, prime the operator for their session. + + + +As an AI builder, I want a quick workspace health briefing at session start, so that I know what needs attention before I start working. + + + +- Start of every work session +- When user says "base pulse", "what's the state of things", "workspace status" +- When the pulse hook detects overdue grooming and injects a prompt +- Entry point routes here via /base:pulse + + + + + +Read workspace state from `.base/workspace.json` and `.base/data/state.json`. + +1. Read `.base/workspace.json` — the manifest +2. Read `.base/data/state.json` — the last known state +3. If either file is missing, suggest running `/base:scaffold` first +4. Extract: last groom date, groom cadence, area list, satellite list + + + +Check each tracked area against filesystem reality. + +For each area in the manifest: +1. Check filesystem timestamps on tracked paths (stat modification dates) +2. Compare against last groom date and area-specific cadence +3. Calculate days overdue (0 if within cadence) +4. Classify: Current (within cadence), Stale (1-2x overdue), Critical (2x+ overdue) + +For each registered satellite: +1. Check if state file exists and is readable +2. Extract last modification date +3. Report current phase if parseable + +Calculate total drift score: sum of days-overdue across all areas, with Critical areas weighted 2x. + + + +Present the health dashboard to the operator. + +Format: +``` +BASE Pulse — {workspace-name} +Last Groom: {date} ({N} days ago) +Drift Score: {score} + +| Area | Status | Age | Due | +|------|--------|-----|-----| +... + +Satellites: +| Project | Phase | Last Active | +... + +{Recommendation based on drift score} +``` + +Recommendations: +- Drift 0: "Workspace is clean. Proceed normally." +- Drift 1-7: "Minor drift in {areas}. Consider grooming this week." +- Drift 8-14: "Moderate drift. Run /base:groom soon." +- Drift 15+: "Critical drift. Workspace context is stale. Run /base:groom now." + + + + + +Health dashboard with drift score, area statuses, satellite health, and recommended next action. + + + +- [ ] All manifest areas checked against filesystem reality +- [ ] Drift score calculated correctly +- [ ] Satellites checked for health +- [ ] Clear recommendation provided based on drift level +- [ ] Dashboard is concise and scannable (not a wall of text) + diff --git a/base-framework/tasks/scaffold.md b/base-framework/tasks/scaffold.md new file mode 100644 index 0000000..24d278c --- /dev/null +++ b/base-framework/tasks/scaffold.md @@ -0,0 +1,389 @@ + +Set up BASE in a new or existing workspace. Scan the workspace, ask guided questions, generate the manifest, install hooks, initialize JSON data surfaces, and run operator profile setup. Optional --full mode adds CLAUDE.md audit and guided first groom. + + + +As an AI builder setting up my workspace, I want a guided scaffolding process that configures workspace management for my specific setup, so that I get maintenance automation without manual configuration. + + + +- First-time BASE installation in any workspace +- When user says "base scaffold", "set up base", "initialize workspace management" +- Entry point routes here via /base:scaffold +- Use --full flag for batteries-included mode with CLAUDE.md audit + first groom + + + +@templates/workspace-json.md + + + + + +Determine scaffold mode. + +1. Check if user specified `--full` or mentioned wanting full setup +2. If `--full`: CLAUDE.md audit + first groom will be offered after data layer setup +3. If standard: data layer + hooks + operator profile +4. Announce mode: "Running BASE scaffold ({standard|full} mode)." + + + +Scan the workspace and detect what exists. + +1. List top-level directories and files +2. Detect common patterns: + - .base/data/ → existing JSON surfaces (v2 data model) + - ACTIVE.md, BACKLOG.md → legacy working memory (offer migration) + - projects/ → project tracking + - apps/ → satellite projects + - tools/ → tool management + - .claude/ → system layer + - .mcp.json → MCP configuration + - content/ → content pipeline + - clients/ → client work + - obsidian/ → knowledge graph + - .carl/ → CARL dynamic rules +3. Detect satellite projects (directories with .paul/ inside apps/) +4. Present findings: "I found: {list of detected areas}" + +**Wait for confirmation before proceeding.** + + + +Walk through each detected area and configure tracking. + +For each detected area: +1. "I found {area}. Want BASE to track this?" +2. If yes: "What grooming cadence? (weekly/bi-weekly/monthly)" +3. Auto-select audit strategy based on area type +4. Allow override of defaults + +Also ask: +- "What day do you prefer for weekly grooming?" (default: Friday) +- "Any directories I should scan for satellite projects?" (default: apps/) +- "Anything else you want tracked that I didn't detect?" + +Build workspace.json from responses using `@templates/workspace-json.md` schema. + + + +Create .base/ directory and generate JSON data surfaces. + +1. Create `.base/` directory structure: + ``` + .base/ + ├── workspace.json + ├── operator.json + ├── data/ + │ ├── projects.json + │ ├── entities.json + │ ├── state.json + │ ├── psmm.json + │ └── staging.json + ├── hooks/ + ├── base-mcp/ + ├── grooming/ + ├── schemas/ + └── audits/ + ``` +2. Write workspace.json from guided configuration (with surfaces and carl_hygiene sections) +3. Initialize JSON data surfaces with empty starter content (don't overwrite existing): + - projects.json — unified active work + backlog tracking + - entities.json — people, organizations, systems + - state.json — workspace health, drift, groom tracking + - psmm.json — per-session meta memory + - staging.json — proposed changes staging +4. Copy operator.json template (don't overwrite existing) +5. Register any detected satellite projects in workspace.json +6. Report: "BASE data layer installed. {N} areas tracked, {N} satellites registered, {N} data surfaces initialized." + + + +Install and register BASE hooks. + +All hooks live in `.base/hooks/`. Session hooks are registered in `.claude/settings.json`. + +**UserPromptSubmit hooks** (fire every prompt): +- active-hook.py — active work surface injection +- backlog-hook.py — backlog surface injection +- base-pulse-check.py — drift detection + groom reminders +- psmm-injector.py — per-session meta memory injection +- operator.py — operator identity context injection + +**SessionStart hooks** (fire once when Claude Code starts a session): +- satellite-detection.py — PAUL project auto-registration and state sync + +**On-demand hooks** (invoked by commands, not auto-registered): +- apex-insights.py — workspace analytics (invoked by /apex:insights) + +--- + +### ENVIRONMENT DETECTION (REQUIRED — do this FIRST) + +Hooks are shell commands that Claude Code executes. The python path AND file paths must work in the context where Claude Code is running. Detect the environment before wiring anything. + +**Step 1: Identify the platform.** +Run these commands and read the results: +```bash +uname -a # Linux vs Darwin vs MINGW/MSYS +cat /proc/version 2>/dev/null # WSL detection (contains "Microsoft" or "WSL") +echo $TERM_PROGRAM # vscode = VS Code integrated terminal +``` + +**Step 2: Classify the environment.** + +| Environment | Detection | Python Command | File Paths | +|---|---|---|---| +| **Native Linux** | `uname` = Linux, no WSL in /proc/version | `which python3` → use result | Native paths work | +| **Native macOS** | `uname` = Darwin | `which python3` → use result (often /opt/homebrew/bin/python3) | Native paths work | +| **WSL Terminal** (Claude Code CLI in WSL) | Linux + "Microsoft" in /proc/version + NOT in VS Code | `which python3` → use result (typically /usr/bin/python3) | WSL paths work (/home/user/...) | +| **VS Code Extension (WSL Remote)** | Linux + WSL + TERM_PROGRAM=vscode | `which python3` → use result | WSL paths work (VS Code server runs inside WSL) | +| **VS Code Extension (Windows-native)** | platform: win32 in Claude Code, OR `uname` returns MINGW/MSYS | See troubleshooting below | Windows paths required | +| **Native Windows** | No WSL, Windows paths | `where python` or `py -3` | Windows paths (C:\...) | + +**Step 3: Handle the tricky cases.** + +**VS Code Extension on Windows accessing WSL files (PROBLEMATIC):** +This is the hardest case. The VS Code extension runs on the Windows side but can see WSL files. Hooks execute in a Windows context, so: +- `/usr/bin/python3` does NOT exist +- `/home/user/...` paths are NOT valid +- The Windows Python stub (`WindowsApps/python3.exe`) can't access WSL paths + +**Solutions (present to user in order of preference):** + +1. **Use VS Code Remote - WSL extension** (RECOMMENDED): + - Install the "WSL" extension in VS Code (by Microsoft) + - Open the workspace with "Reopen in WSL" or `code --remote wsl+Ubuntu /path/to/workspace` + - This runs the VS Code server inside WSL — all hooks fire natively + - All WSL paths and python work correctly + +2. **Use Claude Code CLI in WSL terminal instead of VS Code extension:** + - Open a WSL terminal, `cd` to workspace, run `claude` + - All hooks fire natively in WSL context + - Use VS Code separately for editing if needed + +3. **Wrapper script approach** (for advanced users who need both contexts): + Create a wrapper at a Windows-accessible location that detects context and routes: + ```bash + #!/bin/bash + # Detect if running in WSL or Windows and route accordingly + if [ -f /proc/version ] && grep -qi microsoft /proc/version; then + # Running in WSL context — use WSL python directly + /usr/bin/python3 "$@" + else + # Running in Windows context — invoke via wsl + wsl /usr/bin/python3 "$@" + fi + ``` + This is fragile and NOT recommended for most users. + +**IMPORTANT: Ask the user which environment they use Claude Code in before proceeding.** +If they use multiple environments (e.g., CLI in WSL + VS Code extension), explain the constraints and recommend option 1 (VS Code Remote WSL). + +--- + +### HOOK REGISTRATION + +After environment is classified and python path is determined: + +For each auto-fire hook: +1. Check if `.base/hooks/{hook}` exists +2. If not: copy from `~/.claude/base-framework/hooks/{hook}` (global install source) + - If `~/.claude/base-framework/hooks/{hook}` doesn't exist either, warn: + "BASE framework not globally installed. Run `npx base-framework --global` first, then re-run scaffold." +3. Check `.claude/settings.json` for hook registration: + - **UserPromptSubmit hooks** → register in `UserPromptSubmit` array + - **SessionStart hooks** (satellite-detection.py) → register in `SessionStart` array +4. If not registered: add the hook entry using detected python path + absolute path to `.base/hooks/{hook}` + +Hook registration format in settings.json: + +**CRITICAL: Each event type array contains objects with a `hooks` array inside — NOT flat command objects.** This is the Claude Code settings.json schema. Getting this wrong means hooks silently fail. + +```json +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/active-hook.py" }, + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/backlog-hook.py" }, + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/base-pulse-check.py" }, + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/psmm-injector.py" }, + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/operator.py" } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/satellite-detection.py" } + ] + } + ] + } +} +``` + +**Merge strategy:** If `.claude/settings.json` already has a `hooks` section with existing entries (e.g., from CARL or other tools), APPEND BASE hooks into the existing `hooks` arrays inside the event type objects. Do NOT overwrite existing hooks. Read the file first, find the right array, add entries that aren't already present. + +--- + +### HOOK TROUBLESHOOTING + +If hooks aren't firing after setup, diagnose with these checks: + +**Symptom: "operation blocked by hook" or "No such file"** +- Python path is wrong for the current environment +- Fix: re-detect python path for the environment Claude Code is running in + +**Symptom: Zero hooks fire (no CARL, no pulse, no calendar, nothing)** +- Likely a platform mismatch (Windows paths vs WSL paths) +- Check: `echo $PATH | tr ':' '\n' | grep python` — does python3 resolve? +- Check: Can Claude Code's shell access the hook file? Run `cat {hook_path}` to verify + +**Symptom: Hooks fire in terminal but not in VS Code (or vice versa)** +- Different Claude Code instances run in different contexts +- VS Code extension (Windows-native) ≠ Claude Code CLI (WSL) +- Fix: Use VS Code Remote WSL extension so both contexts are WSL + +**Symptom: "python3: command not found"** +- Python3 is not on PATH in the hook execution context +- Fix: Use absolute path to python3 (detect with `which python3`) + +**Diagnostic command (run this to check hook health):** +```bash +# Test each hook manually +for hook in .base/hooks/*.py; do + echo "--- Testing: $hook ---" + {detected_python3_path} "$hook" 2>&1 | head -3 + echo "Exit code: $?" +done +``` + +Report: "Hooks installed ({N} auto-fire hooks registered, 1 on-demand hook available)." +Report environment: "{environment_type} detected — hooks configured for {python_path}" + + + +Guide the operator through their profile setup. + +1. Check if `.base/operator.json` has completed sections (check `completed_at` fields) +2. If all sections completed: "Operator profile already configured. Want to update any section?" +3. If incomplete or new: + - Walk through each section of operator.json: + a. **Deep Why** — 5 progressively deeper questions about motivation + b. **North Star** — One measurable metric with timeframe + c. **Key Values** — Rank-ordered values with concrete meanings (max 5) + d. **Elevator Pitch** — Layered pitch (1-4 floors) + e. **Surface Vision** — Concrete scenes of what success looks like + - Each section can be skipped: "Skip for now? You can complete it later." + - Write responses to operator.json after each section +4. Report: "Operator profile {complete|partially complete}. The operator hook will inject your identity context every session." + + + +Install and wire the MCP server from the global BASE package. + +The MCP server package lives globally at `~/.claude/base-framework/packages/base-mcp/`. Scaffold copies it into the workspace and wires it up. + +1. Check if `.base/base-mcp/index.js` exists in the workspace +2. If NOT present: + a. Check if global source exists at `~/.claude/base-framework/packages/base-mcp/` + b. If global source missing: warn "BASE framework not globally installed. Run the installer first." + c. If global source exists: copy the entire `base-mcp/` directory to `.base/base-mcp/` + d. Run `npm install` in `.base/base-mcp/` to install dependencies +3. If already present: check for `node_modules/`. If missing, run `npm install`. +4. Check `.mcp.json` for base-mcp registration +5. If not registered: add registration to `.mcp.json`: + ```json + { "base-mcp": { "type": "stdio", "command": "node", "args": ["./.base/base-mcp/index.js"] } } + ``` +6. Report: "BASE MCP server installed from global package and registered. Claude can now manage your data surfaces through tool calls." + + + +**Full mode only.** + +**CLAUDE.md audit:** +1. Check if CLAUDE.md exists +2. If exists: "Want me to audit your CLAUDE.md against the CLAUDE.md Strategy?" + - If yes: route to `/base:audit-claude-md` (interactive, strategy-driven audit with CARL detection) +3. If doesn't exist: "Want me to generate a CLAUDE.md from the strategy template?" + - If yes: use `@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/claudemd-template.md` as starting point, fill from detected workspace structure + +**First groom:** +1. "Want to run an initial groom to establish baseline? This reviews each area once." +2. If yes: run /base:groom flow +3. If no: "Baseline set from filesystem timestamps. First groom due: {date}." + + + +Quick review and cleanup. Catches artifacts from path detection bugs, stale files, or misaligned structure. + +**Run these checks in order:** + +1. **Bogus directories** — Scan workspace root for directories that shouldn't exist: + - Any directory starting with `C:` or containing Windows-style paths (path detection bug) + - Any directory named `undefined`, `null`, or `[object Object]` + - If found: delete them and report what was removed + +2. **Sunset files** — Check for files that no longer belong: + - `.base/data/active.json` → sunset, replaced by projects.json + - `.base/data/backlog.json` → sunset, replaced by projects.json + - `ACTIVE.md` or `BACKLOG.md` at workspace root → legacy, offer to remove + - If found: report and offer to remove (don't auto-delete without confirmation) + +3. **Path sanity** — Verify all registered hooks use correct paths for the current environment: + - Read `.claude/settings.json` hook entries + - Check each hook path exists on disk + - Check python path resolves (`which {python_path}`) + - If any path is invalid: flag it with the correct replacement + +4. **MCP sanity** — Verify MCP registration points to a real file: + - Read `.mcp.json` + - Check `.base/base-mcp/index.js` exists + - Check `node_modules/` exists in `.base/base-mcp/` + - If broken: fix it (copy from global, npm install, re-register) + +5. **Structure alignment** — Verify workspace matches CLAUDE.md's Where section: + - Read CLAUDE.md (if it exists) and extract the Where section + - Compare declared directories against what actually exists + - Flag any mismatches (declared but not created, or created but not declared) + - Don't auto-fix — just report for user awareness + +**Report:** +``` +Post-scaffold cleanup: +- Artifacts removed: {list or "none"} +- Sunset files found: {list or "none"} +- Hook paths: {all valid | N issues} +- MCP: {healthy | issues} +- Structure alignment: {aligned | N mismatches} +``` + +If everything is clean: "Workspace is clean. No artifacts, all paths valid, structure aligned." + + + + + +Fully configured BASE installation. Standard mode: data layer with JSON surfaces, hooks wired, operator profile setup, MCP registered, post-scaffold cleanup verified. Full mode: adds CLAUDE.md audit and guided first groom. + + + +- [ ] Workspace scanned and areas detected +- [ ] Operator confirmed tracked areas and cadences +- [ ] .base/ directory created with all required files +- [ ] workspace.json generated from guided configuration +- [ ] JSON data surfaces initialized (projects, entities, state, psmm, staging) +- [ ] operator.json created and profile questionnaire offered +- [ ] Satellite projects detected and registered +- [ ] All auto-fire hooks installed and registered in settings.json (UserPromptSubmit + SessionStart) +- [ ] BASE MCP server wired in .mcp.json +- [ ] Post-scaffold cleanup passed (no artifacts, valid paths, structure aligned) +- [ ] (Full mode) CLAUDE.md audit offered +- [ ] (Full mode) First groom offered +- [ ] Operator informed of next groom date + diff --git a/base-framework/tasks/status.md b/base-framework/tasks/status.md new file mode 100644 index 0000000..4d950e7 --- /dev/null +++ b/base-framework/tasks/status.md @@ -0,0 +1,35 @@ + +Quick one-liner workspace health check. No conversation, just the numbers. + + + +As an AI builder, I want a fast health check I can glance at, so that I know if anything needs attention without a full briefing. + + + +- When user wants a quick check without full pulse +- Entry point routes here via /base:status + + + + + +Read state and output one-liner. + +1. Read `.base/data/state.json` +2. Calculate current drift score from timestamps +3. Count overdue areas and past-due backlog items +4. Output single line: "BASE: Drift {score} | {N} areas overdue | {N} backlog items past review-by | Last groom: {date}" + + + + + +Single-line health summary. No conversation. + + + +- [ ] Output is one line +- [ ] Drift score is current (not cached) +- [ ] Overdue counts are accurate + diff --git a/base-framework/tasks/surface-convert.md b/base-framework/tasks/surface-convert.md new file mode 100644 index 0000000..372aebd --- /dev/null +++ b/base-framework/tasks/surface-convert.md @@ -0,0 +1,143 @@ + +Convert an existing @-mentioned markdown file into a structured data surface. Analyzes the markdown structure, proposes a schema, migrates content, and generates all surface artifacts (JSON, hook, registration). + + + +As an AI builder, I want to convert my existing markdown tracking files into structured surfaces so Claude gets cheap passive awareness instead of expensive @-file parsing. + + + +- /base:surface convert {file-path} +- "convert this file to a surface", "make this a data surface" +- User has a markdown file they @-mention regularly and wants it structured + + + +@.base/hooks/_template.py +@.base/workspace.json + + + + + +## Step 1: Read & Analyze + +1. Read the specified markdown file completely +2. Detect structure: + - **Headings** → potential categories or priority groups + - **Bold labels** (e.g., `**Status:**`) → field names + - **List items** → individual entries + - **Checkboxes** → checklist/progress fields + - **Dates** → timestamp fields + - **File paths** → location fields + - **Tables** → structured data (archived items, reference tables) +3. Identify patterns: + - How many distinct items? + - What fields recur across items? + - Are there priority/status groupings? + - Is there an archived/done section? + +Present findings: +``` +Detected structure in {file}: + Items found: {count} + Sections: {list of heading-based groups} + Recurring fields: {field names} + Archived section: {yes/no} +``` + + + +## Step 2: Propose Schema + +Based on analysis, propose: + +1. **Surface name** — infer from filename (e.g., ACTIVE.md → "active") +2. **Schema** — field names, types, required fields, ID prefix +3. **Sample conversion** — show 2-3 items converted to JSON + +``` +Proposed schema for "{name}": + ID prefix: {PREFIX} + Required: {fields} + Optional: {fields} + Priority levels: {if detected} + +Sample conversion: + "{heading item}" → + { + "id": "PREFIX-001", + "title": "...", + "status": "...", + ... + } +``` + +Ask: "Does this schema look right? Adjust anything?" + +**Wait for response.** + + + +## Step 3: Confirm + +Apply any user adjustments to the schema. Lock it for generation. + +If user is satisfied, confirm: +"Schema locked. I'll generate the surface and migrate {count} items." + + + +## Step 4: Generate Artifacts + +Same generation as surface-create Step 5: +- `.base/data/{name}.json` — with migrated items (not empty) +- `.base/hooks/{name}-hook.py` — from _template.py with appropriate grouping +- workspace.json surface registration +- settings.json hook entry + +Include staleness detection with sensible defaults based on detected priority levels. + + + +## Step 5: Migrate Content + +Parse every item from the markdown into JSON entries: +- Map heading groups to priority/category fields +- Map bold labels to field values +- Preserve checklists as arrays of {text, done} objects +- Preserve dates in ISO format +- Preserve file paths as location fields +- Map done/closed/archived sections to the archived array + +Report: +``` +Migration complete: + Items migrated: {count} + Archived items: {count} + Unmapped items: {count, if any — list them} +``` + +If any items couldn't be auto-mapped, present them for manual resolution. + + + +## Step 6: Clean Up + +1. Check if the original file is @-referenced in CLAUDE.md +2. If found, offer: "Remove @{file} from CLAUDE.md? The surface hook replaces it." +3. Suggest: "Original file preserved at {path} for reference." + +Do NOT delete the original markdown file — the user decides its fate. + + + + + +After conversion: +- [ ] .base/data/{name}.json exists with migrated items +- [ ] Item count matches source markdown +- [ ] .base/hooks/{name}-hook.py exists and produces output +- [ ] workspace.json and settings.json updated +- [ ] Original markdown file is untouched + diff --git a/base-framework/tasks/surface-create.md b/base-framework/tasks/surface-create.md new file mode 100644 index 0000000..c679bb3 --- /dev/null +++ b/base-framework/tasks/surface-create.md @@ -0,0 +1,184 @@ + +Create a new data surface through guided conversation. Generates: JSON data file, injection hook, workspace.json registration, and settings.json hook entry. The user answers questions; Claude generates everything. + + + +As an AI builder, I want to create custom data surfaces so Claude has structured, passive awareness of any domain I track — without manually wiring JSON files, hooks, and config. + + + +- /base:surface create {name} +- "create a surface", "add a new surface", "I want to track X" +- User wants structured data with hook injection and MCP access + + + +@.base/hooks/_template.py +@.base/workspace.json + + + + + +## Step 1: Define + +Extract surface name from args or ask: "What should this surface be called? (lowercase, no spaces)" + +**Validate:** +1. Name is lowercase, alphanumeric + hyphens only +2. Not already registered in workspace.json surfaces section +3. No reserved names: "active", "backlog", "psmm", "staging" + +Ask: "What does this surface track? (one sentence)" +→ This becomes the `description` in workspace.json. + +**Wait for response before proceeding.** + + + +## Step 2: Schema + +Ask: "What fields does each item need?" + +Guide through these decisions (one at a time): + +1. **Required fields** — What must every item have? + - Default minimum: `["title"]` + - Common additions: status, priority, category, assignee, due_date + +2. **ID prefix** — Auto-suggest first 3 chars of name, uppercase. + - e.g., surface "clients" → prefix "CLI" + - User can override + +3. **Priority/status enums** — If the surface has priority or status fields: + - Ask: "What priority levels?" (e.g., high, medium, low) + - Ask: "What status values?" (e.g., active, pending, done) + +4. **Time rules** (optional) — If the surface benefits from review-by dates: + - Ask: "Should items have review-by deadlines? If so, how many days per priority level?" + - Default: none + +Build the schema object from answers: +```json +{ + "id_prefix": "CLI", + "required_fields": ["title", "status"], + "priority_levels": ["high", "medium", "low"], + "status_values": ["active", "pending", "done"] +} +``` + +**Wait for response before proceeding.** + + + +## Step 3: Injection + +Ask: "How should items appear in Claude's context each prompt?" + +Guide through: + +1. **Grouping** — How to organize items in the injection? + - By priority (default) | By status | By date | Flat (no grouping) + +2. **Summary format** — What fields to show per line? + - Default: `- [ID] Title (status)` + - Can add: priority tag, due date, custom field + +3. **Staleness thresholds** — Days before an item is flagged STALE per priority: + - Suggest defaults based on priority levels from Step 2 + - e.g., high: 5d, medium: 10d, low: 30d + - User can adjust + +4. **Behavioral mode:** + - Silent (default) — passive awareness, respond only when asked + - Proactive — mention items unprompted (rare, use for critical surfaces) + - Threshold — stay silent unless deadline/staleness threshold crossed + +**Wait for response before proceeding.** + + + +## Step 4: Tools (Informational) + +Inform the user: + +"All 7 BASE MCP tools work automatically with your new surface: +- `base_list_surfaces` — see all surfaces +- `base_get_surface("{name}")` — read all items +- `base_get_item("{name}", id)` — get specific item +- `base_add_item("{name}", data)` — add new item (validates required fields, auto-generates ID) +- `base_update_item("{name}", id, data)` — update fields (resets staleness clock) +- `base_archive_item("{name}", id)` — move to archived +- `base_search(query, "{name}")` — search items + +No configuration needed — BASE MCP auto-discovers surfaces from workspace.json." + +**Continue to generation.** + + + +## Step 5: Generate + +Create all artifacts: + +**1. Data file:** `.base/data/{name}.json` +```json +{ + "surface": "{name}", + "version": 1, + "last_modified": "{timestamp}", + "items": [], + "archived": [] +} +``` + +**2. Hook file:** `.base/hooks/{name}-hook.py` +- Read `.base/hooks/_template.py` as the starting point +- Customize SURFACE_NAME, grouping logic, summary format, staleness thresholds, behavioral directive +- Use the injection decisions from Step 3 +- Include `from datetime import date` for staleness calculation +- Follow the _template.py contract exactly + +**3. Registration:** Add to `.base/workspace.json` surfaces section: +```json +"{name}": { + "file": "data/{name}.json", + "description": "{description from Step 1}", + "hook": true, + "silent": true, + "schema": { ...schema from Step 2... } +} +``` + +**4. Hook registration:** Add to `.claude/settings.json` UserPromptSubmit hooks: +```json +{ + "type": "command", + "command": "python3 {absolute_workspace_path}/.base/hooks/{name}-hook.py" +} +``` +Use absolute path resolved from workspace root. + +**5. Report:** +``` +Surface "{name}" created: + Data: .base/data/{name}.json (empty, ready for items) + Hook: .base/hooks/{name}-hook.py (will inject next prompt) + Schema: {id_prefix}-NNN, {required_fields} + Tools: base_get_item("{name}", id), base_add_item("{name}", data), etc. + +Add your first item: base_add_item("{name}", {title: "..."}) +``` + + + + + +After generation: +- [ ] .base/data/{name}.json exists and is valid JSON +- [ ] .base/hooks/{name}-hook.py exists and parses as valid Python +- [ ] workspace.json has the surface registration +- [ ] settings.json has the hook entry with absolute path +- [ ] Hook outputs correct XML when piped test input + diff --git a/base-framework/tasks/surface-list.md b/base-framework/tasks/surface-list.md new file mode 100644 index 0000000..ce154f6 --- /dev/null +++ b/base-framework/tasks/surface-list.md @@ -0,0 +1,42 @@ + +Display all registered data surfaces with item counts, hook status, and staleness summary. + + + +- /base:surface list +- "what surfaces exist", "show surfaces", "list surfaces" + + + + + +## Show Surfaces + +1. Call `base_list_surfaces` to get all registered surfaces with item counts +2. For each surface, read the data file to count stale items (items with no `updated` field or `updated` older than threshold) + +Display as: + +``` +Data Surfaces +═══════════════════════════════════════ + +| Surface | Items | Hook | Description | +|---------|-------|------|-------------| +| active | 12 | ✓ | Active work items, projects, and tasks | +| backlog | 8 | ✓ | Future work queue, ideas, and deferred tasks | + +Total: {count} surfaces, {total_items} items + +Create a new surface: /base:surface create {name} +Convert a file: /base:surface convert {path} +``` + +If no surfaces registered: +``` +No data surfaces registered. +Run /base:surface create {name} to create your first surface. +``` + + + diff --git a/base-framework/tasks/weekly-domain-create.md b/base-framework/tasks/weekly-domain-create.md new file mode 100644 index 0000000..80772b9 --- /dev/null +++ b/base-framework/tasks/weekly-domain-create.md @@ -0,0 +1,173 @@ + +Guided creation of a custom domain phase for /base:weekly. Walks the user through defining what to check, what tools to pull data from, what questions to ask weekly, where the phase fits in the flow, and what output it produces. Every domain phase goes through this same workflow — no presets, no shortcuts. + + + +As an AI builder, I want to add custom check-in areas to my weekly ritual, so that the weekly covers everything that matters to me without being locked into someone else's priorities. + + + +- When operator wants to add a domain to their weekly +- After first /base:weekly run (offered when no domain phases exist) +- When operator says "add a domain to my weekly", "I want to track X weekly" +- Entry point routes here via /base:weekly-domain + + + + + +Understand what the operator wants to track weekly. + +1. Ask: "What area of your week needs its own check-in?" +2. Offer starter ideas to help them think clearly: + - "Some common ones people create: revenue tracking, content pipeline, client follow-ups, team sync, health/fitness check, learning log, community engagement." + - "Or describe something entirely your own." +3. Wait for response +4. Clarify if needed: "So this phase would check on {paraphrase}. What would make this useful to you every week?" + +**Wait for clear description before proceeding.** + + + +Identify data sources and tools for this domain. + +1. Ask: "What data should this phase pull? What tools or sources are relevant?" + - Give examples: "Calendar events in a category, Slack channel messages, MCP server data, project statuses, external dashboard" +2. If the operator is unsure: + a. Search available MCP tools (use tool listing / grep for relevant keywords) + b. Present relevant ones: "I found these tools that might be useful: {list}" + c. Let operator pick or decline +3. If a needed tool doesn't exist: + - Note it: "That tool doesn't exist yet. I'll note it as a future backlog item." + - The domain phase still works — it just skips that data source at runtime +4. For each selected data source, capture: + - Tool name (MCP tool ID) + - Parameters to pass + - Human-readable label + +**Build the data_sources[] array from this conversation.** + + + +Define the weekly questions this phase asks. + +1. Ask: "What questions should this phase ask you every week?" + - Give examples based on their domain: + - Revenue: "Did I hit my revenue target? What's the pipeline value? Any invoices pending?" + - Content: "How many pieces published? What's queued? Am I on cadence?" + - Custom: derive from their description +2. Capture each question as a string +3. Confirm the list: "These are the questions for your {name} phase: {list}. Adjust?" + +**Wait for confirmation.** + + + +Determine where this phase runs in the weekly flow. + +Present the weekly structure: +``` +1. Week Review +2. Calendar Audit +3. Workspace Groom +4. Priority Stack +5. Backlog Triage + --- domain phases run here --- +6. Blockers & Delegation +7. Week Commit +``` + +Ask: "Where should this phase run? Most domain phases run after backlog triage (position 5) and before blockers (position 6). Sound right, or do you want it somewhere else?" + +Options: +- `after_groom` — runs after Phase 3 +- `after_priorities` — runs after Phase 4 +- `after_backlog` — runs after Phase 5 (default, recommended) +- `before_blockers` — same as after_backlog unless other domains exist (controls ordering among domains) + +**Wait for response.** + + + +Determine what this phase produces. + +Ask: "What should this phase output?" +Options: +- **Notes** — just captures your responses as part of the weekly record +- **Calendar events** — creates specific calendar blocks (e.g., "content recording session") +- **Project updates** — updates project statuses or adds tasks +- **Mixed** — any combination + +For each output type, capture specifics: +- Calendar: what kind of events, default duration, which calendar +- Project: which project ID to update, what fields +- Notes: just stored in weekly entry (default, always happens) + +**Wait for response.** + + + +Generate the domain phase config and save to weekly.json. + +1. Compose the domain phase object: + ```json + { + "id": "{kebab-case-id}", + "name": "{Display Name}", + "description": "{one-line description}", + "position": "{chosen position}", + "data_sources": [ + { + "type": "mcp", + "tool": "{tool_name}", + "params": {}, + "label": "{human label}" + } + ], + "questions": [ + "{question 1}", + "{question 2}" + ], + "outputs": { + "type": "notes|calendar|project_update|mixed", + "details": {} + }, + "enabled": true, + "created": "{ISO date}" + } + ``` +2. Read current `.base/weekly.json` +3. Append to `domain_phases[]` +4. Write updated weekly.json +5. Present: + ``` + Domain phase created: {name} + Position: {where it runs} + Data sources: {list} + Questions: {count} + Output: {type} + + This will run during your next /base:weekly. + Want to create another domain phase, or are you done? + ``` + +**Wait for response.** + + + + + +New domain phase config added to `.base/weekly.json` -> `domain_phases[]`. Ready to execute on next /base:weekly run. + + + +- [ ] Operator described the domain area clearly +- [ ] Tool discovery performed — available MCP tools searched if operator was unsure +- [ ] Data sources identified and captured with tool names + params +- [ ] Weekly questions defined and confirmed +- [ ] Position in weekly flow chosen +- [ ] Output type specified +- [ ] Domain phase config written to weekly.json +- [ ] Config is valid JSON matching the schema +- [ ] Operator informed of next steps + diff --git a/base-framework/tasks/weekly.md b/base-framework/tasks/weekly.md new file mode 100644 index 0000000..b293e95 --- /dev/null +++ b/base-framework/tasks/weekly.md @@ -0,0 +1,347 @@ + +Guided weekly review and planning ritual. Walks the operator through 8 core phases sequentially, executes custom domain phases, and commits the week with calendar events created. Designed as one of two cadence rituals (daily closes the day, weekly closes the week and opens the next). + + + +As an AI builder, I want a structured weekly ritual that reviews my week, plans the next, maintains my workspace, and locks in priorities with real calendar events, so that I start every week with clarity instead of reacting to whatever's loudest. + + + +- Weekly (typically Sunday evening) +- When operator says "run my weekly", "weekly review", "time for my weekly" +- Entry point routes here via /base:weekly + + + + + +Load config and establish session context. + +1. Read `.base/weekly.json` + - If file doesn't exist: create it with empty defaults (see schema below) + - Parse: domain_phases[], calendar_rules[], daily_logs[], history[] +2. Get last weekly entry from history[] (if any) + - Note: week_of, priorities set, priorities completed +3. Get current date context (from hook injection or system) +4. Present: + ``` + Weekly — Week of {Monday date} to {Sunday date} + Last weekly: {date or "first run"} + Daily logs this week: {count} + Domain phases configured: {count} + ``` +5. "Ready to start? (You can skip any phase by saying 'skip')" + +**Wait for confirmation.** + + + +Phase 1: Review the past week. + +**If daily logs exist** (daily_logs[] entries from the past 7 days): +1. Summarize: days logged, patterns in wins/misses, energy trends +2. Note any recurring blockers or themes +3. Present the summary to the operator + +**If no daily logs:** +1. Note: "No daily logs found this week. Phase 1 is reflective-only." + +**Always ask:** +- "What went well this week?" +- "What didn't land?" +- "Anything surprising or worth noting?" + +Capture the operator's reflection. This goes into the weekly entry. + +**Wait for responses.** + + + +Phase 2: Audit and plan the calendar. + +1. Use `list_events` MCP tool — pull events for the next 7 days + - If multiple calendars available (personal + family), pull all + - If calendar MCP unavailable: skip to manual questions, note the gap +2. Apply display rules: show all events with real titles during audit (rules only apply on creation) +3. Present the week's schedule in a clean format: + ``` + Monday 3/31: + 9:00 AM — Meeting with Charlie + 2:00 PM — Coaching call (Amee) + Tuesday 4/1: + (open) + ... + ``` +4. Ask: + - "Anything missing from the calendar?" + - "Where do you want deep work blocks?" + - "Any family commitments to add?" + - "Any conflicts to resolve?" +5. Collect requested additions/changes (created in Phase 8) + +**Wait for responses.** + + + +Phase 3: Workspace maintenance. + +Run groom logic inline — NOT as a separate /base:groom invocation. + +1. Read drift score from `base_get_state` or state.json +2. Read stale areas from base-pulse data +3. Walk through each stale area: + - Projects: quick status check on active/blocked items + - Clients: any updates needed? + - Content: pipeline current? + - Other flagged areas from pulse +4. For each area reviewed: + - Update timestamps via `base_update_area` + - Note changes made +5. Record groom via `base_record_groom` +6. Update drift via `base_update_drift` +7. Report: "Drift score: {before} -> {after}. {N} areas groomed." + +**Voice-friendly: walk through one area at a time, wait for response on each.** + + + +Phase 4: Set the week's priorities. + +1. Pull context: + - Operator north star (from operator.json / hook data) + - Active projects with upcoming deadlines (from active-awareness) + - Stale urgent/high items + - Last week's priorities and their status (from previous weekly entry) +2. If previous priorities exist: + - Report: "Last week's priorities: {list}. Status: {completed/carried/dropped}" +3. Suggest 3-5 outcome-based priorities: + - Frame as outcomes, not tasks: "Ship X" not "Work on X" + - Align each to north star or active project + - Weight toward revenue-generating and deadline-driven work +4. Present: "Here are my suggested priorities for this week: {list}. Adjust?" +5. Finalize the stack after operator input + +**Wait for approval or adjustments.** + + + +Phase 5: Process the backlog. + +1. Pull backlog items via `base_list_projects` (status=backlog) +2. Identify items with: + - review_by date passed (overdue) + - review_by date within 7 days (upcoming) + - No review_by date and older than 14 days (stale) +3. Present overdue items first: "These are past their review date: {list}" +4. For each flagged item, ask: + - **Keep** — set new review_by date + - **Graduate** — move to active (update status via `base_update_project`) + - **Kill** — archive via `base_archive_project` +5. After processing flagged items: "Anything new to add to the backlog?" +6. Capture new items via `base_add_project` (status=backlog) + +**Walk through one item at a time.** + + + +Phase 6: Execute custom domain phases. + +1. Read `weekly.json` -> `domain_phases[]` (only enabled ones) +2. If no domain phases configured: + - "No domain phases configured. You can add one anytime with /base:weekly-domain." + - Skip to Phase 7 +3. Sort by configured position (after_groom, after_priorities, after_backlog, before_blockers) +4. For each domain phase: + a. Announce: "Domain phase: {name} — {description}" + b. Pull data from configured data_sources[]: + - For each source: call the specified MCP tool with configured params + - If tool unavailable: note it, continue with remaining sources + c. Present pulled data + d. Ask configured questions[] one at a time + e. Capture responses + f. Produce configured output (notes, calendar events, project updates) +5. Store domain phase results in weekly entry -> domains{} + +**Each domain phase is self-contained — failure in one doesn't block others.** + + + +Phase 7: Identify blockers and queue follow-ups. + +**If Slack MCP available:** +1. Pull recent messages from relevant channels/DMs (last 7 days) +2. Surface threads with pending action or unanswered questions +3. Present: "Here are open threads that may need follow-up: {list}" +4. For each: "Send a nudge this week? (becomes a calendar reminder or note)" + +**If Slack MCP unavailable:** +1. Ask: "What's currently blocked?" +2. Ask: "Who do you need to follow up with this week?" + +**Always:** +3. Cross-reference with active projects that have `blocked` status +4. Generate follow-up list: person, action, urgency +5. Ask which follow-ups should become calendar reminders + +**Wait for responses.** + + + +Phase 8: Lock in the week. + +1. Summarize everything from this session: + ``` + WEEK COMMIT — {date range} + + Priorities: + 1. {outcome} + 2. {outcome} + ... + + Calendar changes: + - {new event/block} on {day} + ... + + Groom: Drift {before} -> {after} + Backlog: {N} reviewed, {N} graduated, {N} killed, {N} new + Follow-ups: {list} + ``` +2. Ask: "Confirm? I'll create the calendar events and log the weekly." + +**On confirmation:** +3. Create calendar events via `create_event` MCP tool: + - Deep work blocks + - Follow-up reminders + - Any additions from Phase 2 + - Apply calendar_rules[] to event titles on creation: + - For each rule where type=title_transform and enabled=true: + - If event title matches rule.match regex: replace with rule.replace + - If rule.calendars specified: only apply to those calendars +4. Write weekly entry to `weekly.json` -> `history[]`: + - Include all phase outputs (review, calendar, groom, priorities, backlog, domains, blockers) + - Compute changeover metrics vs previous entry (priorities carried/completed/dropped, drift delta, backlog net) +5. Report: + ``` + Weekly complete. + {N} calendar events created. + Drift score: {X}. + Next weekly: {suggested date}. + ``` + +**Wait for confirmation before creating events.** + + + + + + +## weekly.json — Initial Empty Config + +When weekly.json doesn't exist, create with: + +```json +{ + "version": "1.0", + "created": "{ISO date}", + "calendar_rules": [], + "domain_phases": [], + "daily_logs": [], + "history": [] +} +``` + +## Daily Log Entry Schema (forward-looking — consumed by Phase 1, written by /base:daily) + +```json +{ + "date": "YYYY-MM-DD", + "logged_at": "ISO datetime", + "reflection": "string", + "wins": ["string"], + "misses": ["string"], + "energy": "high|medium|low", + "domains": { + "domain_id": { + "activities": ["string"], + "metrics": {} + } + }, + "blockers_surfaced": ["string"], + "tomorrow_intent": "string" +} +``` + +## Weekly History Entry Schema (written by Phase 8) + +```json +{ + "id": "unique-id", + "week_of": "YYYY-MM-DD (Monday)", + "run_date": "YYYY-MM-DD", + "run_at": "ISO datetime", + "review": { + "reflection": "string", + "daily_logs_count": 0, + "patterns": ["string"] + }, + "calendar": { + "events_existing": 0, + "events_created": 0, + "conflicts_resolved": 0, + "deep_work_blocks": 0 + }, + "groom": { + "drift_score_before": 0, + "drift_score_after": 0, + "stale_areas_resolved": [], + "projects_touched": 0 + }, + "priorities": [ + { + "outcome": "string", + "aligned_to": "project_id or north_star", + "status": "pending" + } + ], + "backlog": { + "items_reviewed": 0, + "graduated": 0, + "deferred": 0, + "killed": 0, + "new_captured": 0 + }, + "domains": {}, + "blockers": { + "identified": 0, + "follow_ups_queued": 0, + "resolved_since_last": 0 + }, + "changeover": { + "priorities_carried_over": 0, + "priorities_completed": 0, + "priorities_dropped": 0, + "drift_delta": 0, + "backlog_net": 0 + } +} +``` + + + + +Weekly entry logged to weekly.json. Calendar events created. Workspace groomed. Priorities locked. Backlog current. Operator walks away with a clear week ahead. + + + +- [ ] Weekly.json loaded or created on first run +- [ ] Phase 1: Daily logs consumed (if available), reflection captured +- [ ] Phase 2: Calendar pulled and reviewed, additions collected +- [ ] Phase 3: Groom executed inline, drift score updated +- [ ] Phase 4: 3-5 outcome-based priorities set, aligned to north star +- [ ] Phase 5: Overdue backlog items surfaced and processed +- [ ] Phase 6: Domain phases executed (if configured), results captured +- [ ] Phase 7: Blockers identified, follow-ups queued +- [ ] Phase 8: Summary confirmed, calendar events created with rules applied, weekly entry logged +- [ ] Each phase skippable without breaking the flow +- [ ] Operator confirmed at each decision point (voice-friendly pacing) + diff --git a/base-framework/templates/claudemd-template.md b/base-framework/templates/claudemd-template.md new file mode 100644 index 0000000..075c28a --- /dev/null +++ b/base-framework/templates/claudemd-template.md @@ -0,0 +1,102 @@ +# CLAUDE.md Template + +Reference template for generating strategy-compliant CLAUDE.md files. Placeholders use `{PLACEHOLDER}` format. Comments use `` and must be removed in final output. + +*** + +```Markdown +# CLAUDE.md + +## What + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +--- + +## Why + + + +{PHILOSOPHY} + +--- + +## Who + + + +**{WORKSPACE_NAME}** — {ONE_LINE_DESCRIPTION} + +### {BUSINESS_1_NAME} +{BUSINESS_1_URL_IF_APPLICABLE} +- {SERVICE_OR_PRODUCT_1} +- {SERVICE_OR_PRODUCT_2} +- {SERVICE_OR_PRODUCT_3} + + + + + +--- + +## Where + + + +` ` ` +{WORKSPACE_ROOT}/ +├── {DIR_1}/ # {PURPOSE} +├── {DIR_2}/ # {PURPOSE} +│ ├── {SUBDIR}/ # {PURPOSE — only if meaningful} +│ └── {SUBDIR}/ # {PURPOSE} +└── {DIR_N}/ # {PURPOSE} +` ` ` + +--- + +## How + +### Systems + + + +| System | Purpose | Location | +|--------|---------|----------| +| {SYSTEM_1} | {ONE_LINE_PURPOSE} | `{LOCATION}` | +| {SYSTEM_2} | {ONE_LINE_PURPOSE} | `{LOCATION}` | + +### Git Strategy + + + +| Directory | Approach | +|-----------|----------| +| {DIR} | {STRATEGY} | + +### Rules + + + + +NEVER {WRONG_ACTION} — {RIGHT_ACTION} +NEVER {WRONG_ACTION} — {RIGHT_ACTION} + +### Quick Reference + + + +**{ACTION}?** → {INSTRUCTION} +**{ACTION}?** → {INSTRUCTION} +``` + +*** + +## Usage Notes + +* Target: under 100 lines in final output +* Remove all `` comments before finalizing +* Remove placeholder sections that don't apply (not every workspace needs Git Strategy or Systems) +* The Where tree should reflect the ACTUAL filesystem, verified by scanning +* Rules should be workspace-identity-level, not operational. If a rule only applies during specific work (e.g., "when writing tests..."), it belongs in a domain-specific rule system, not CLAUDE.md +* `@` references point Claude to files it should read on demand — use for anything volatile or detailed + diff --git a/base-framework/templates/workspace-json.md b/base-framework/templates/workspace-json.md new file mode 100644 index 0000000..97688f7 --- /dev/null +++ b/base-framework/templates/workspace-json.md @@ -0,0 +1,96 @@ +# Workspace Manifest Template + +Output file: `.base/workspace.json` + +```template +{ + "workspace": "{workspace-name}", + "created": "{YYYY-MM-DD}", + "groom_cadence": "{weekly|bi-weekly|monthly}", + "groom_day": "{day-of-week}", + "areas": { + "{area-name}": { + "type": "{working-memory|directory|config-cross-ref|system-layer|custom}", + "description": "[Human-readable purpose of this area]", + "paths": ["{file-or-directory-paths}"], + "groom": "{weekly|bi-weekly|monthly}", + "audit": { + "strategy": "{staleness|classify|cross-reference|dead-code|pipeline-status}", + "config": {} + } + } + }, + "carl_hygiene": { + "proactive": true, + "cadence": "monthly", + "staleness_threshold_days": 60, + "max_rules_per_domain": 15, + "last_run": null + }, + "surfaces": { + "{surface-name}": { + "file": "data/{name}.json", + "description": "[What this surface tracks]", + "hook": true, + "silent": true, + "schema": { + "id_prefix": "{PREFIX}", + "required_fields": ["{field1}", "{field2}"], + "priority_levels": ["{level1}", "{level2}"], + "status_values": ["{status1}", "{status2}"] + } + } + }, + "satellites": { + "{project-name}": { + "path": "{relative-path-to-project}", + "engine": "{paul|custom|none}", + "state": "{path-to-state-file}", + "registered": "{YYYY-MM-DD}", + "groom_check": true, + "last_activity": null, + "phase_name": null, + "phase_number": null, + "phase_status": null, + "loop_position": "IDLE", + "handoff": false, + "last_plan_completed_at": null + } + } +} +``` + +## Field Documentation + +| Field | Type | Description | +|-------|------|------------| +| workspace | string | Name of this workspace (typically the directory name) | +| created | date | When BASE was initialized in this workspace | +| groom_cadence | enum | Default grooming frequency for the workspace | +| groom_day | string | Preferred day for weekly grooming | +| areas | object | Map of tracked workspace areas | +| areas.*.type | enum | Classification of the area for audit strategy selection | +| areas.*.paths | array | Files or directories this area tracks | +| areas.*.groom | enum | Grooming frequency for this specific area (overrides default) | +| areas.*.audit.strategy | enum | Which audit strategy to apply (see audit-strategies.md) | +| areas.*.audit.config | object | Strategy-specific configuration | +| carl_hygiene | object | CARL rule lifecycle management config (optional — only if CARL is installed) | +| carl_hygiene.proactive | boolean | Auto-surface stale rules during groom | +| carl_hygiene.cadence | enum | How often to run CARL hygiene | +| carl_hygiene.staleness_threshold_days | number | Days before a rule is flagged as stale | +| carl_hygiene.max_rules_per_domain | number | Soft cap per domain (warn, not enforce) | +| surfaces | object | Registered data surfaces with schemas | +| surfaces.*.file | string | Path to JSON file relative to .base/ | +| surfaces.*.hook | boolean | Whether a hook auto-injects this surface | +| surfaces.*.silent | boolean | Whether hook output is passive (no proactive mentions) | +| surfaces.*.schema | object | Validation schema for surface items | +| surfaces.*.schema.id_prefix | string | Auto-generated ID prefix (e.g., "ACT", "BL") | +| surfaces.*.schema.required_fields | array | Fields required on every item | +| satellites | object | External projects tracked by BASE but managed by their own engines | +| satellites.*.engine | enum | What orchestration tool manages this project | +| satellites.*.state | string | Path to the project's state file for health checks | +| satellites.*.groom_check | boolean | Whether BASE checks this project's health during groom (default: true) | +| satellites.*.last_activity | string | ISO timestamp of last project activity (synced from paul.json) | +| satellites.*.phase_name | string | Current phase name (synced from paul.json) | +| satellites.*.loop_position | string | PAUL loop state: IDLE, PLAN, APPLY, UNIFY | +| satellites.*.handoff | boolean | Whether a handoff file exists for this project | diff --git a/base-framework/utils/scan-claude-dirs.py b/base-framework/utils/scan-claude-dirs.py new file mode 100644 index 0000000..3b9919b --- /dev/null +++ b/base-framework/utils/scan-claude-dirs.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +""" +scan-claude-dirs.py — Exhaustive .claude/ directory scanner for BASE audit-claude workflow. + +Produces a structured JSON dataset of every .claude/ directory in a workspace, +including baselines (global ~/.claude/, workspace root .claude/, MCP registry). +Every file gets an MD5 hash. No judgment, no classification — pure data collection. + +Usage: + python3 scan-claude-dirs.py [--workspace ] [--global-config ] [--output ] + +Defaults: + --workspace Current working directory + --global-config ~/.claude + --output .base/audits/data-sets/claude-scan-{date}.json + +The audit-claude workflow reads this JSON and performs classification/planning +against a complete, verified dataset instead of ad-hoc bash commands. +""" + +import argparse +import hashlib +import json +import os +from datetime import datetime, timezone + +# Directories to skip during recursive scan +SKIP_PATTERNS = { + 'node_modules', '_archive', '.git', 'vendor', 'dist', 'build', + '__pycache__', '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache' +} + + +def md5_file(filepath): + """Compute MD5 hash of a file.""" + try: + h = hashlib.md5() + with open(filepath, 'rb') as f: + for chunk in iter(lambda: f.read(8192), b''): + h.update(chunk) + return h.hexdigest() + except (OSError, PermissionError): + return None + + +def file_line_count(filepath): + """Count lines in a text file.""" + try: + with open(filepath, 'r', errors='replace') as f: + return sum(1 for _ in f) + except (OSError, PermissionError): + return None + + +def last_modified(filepath): + """Get last modification time as ISO string.""" + try: + ts = os.path.getmtime(filepath) + return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() + except OSError: + return None + + +def dir_last_modified(dirpath): + """Get the most recent modification time of any file in a directory tree.""" + latest = 0 + try: + for root, dirs, files in os.walk(dirpath): + for f in files: + fp = os.path.join(root, f) + try: + mt = os.path.getmtime(fp) + if mt > latest: + latest = mt + except OSError: + pass + except OSError: + pass + if latest == 0: + return None + return datetime.fromtimestamp(latest, tz=timezone.utc).isoformat() + + +def scan_files_in_dir(dirpath, relative_to=None): + """List all files in a directory (non-recursive) with metadata.""" + results = [] + if not os.path.isdir(dirpath): + return results + try: + for name in sorted(os.listdir(dirpath)): + fp = os.path.join(dirpath, name) + if os.path.isfile(fp): + entry = { + 'name': name, + 'md5': md5_file(fp), + 'lines': file_line_count(fp), + 'size_bytes': os.path.getsize(fp), + 'last_modified': last_modified(fp) + } + if relative_to: + entry['relative_path'] = os.path.relpath(fp, relative_to) + results.append(entry) + except (OSError, PermissionError): + pass + return results + + +def scan_files_recursive(dirpath, relative_to=None): + """List all files in a directory recursively with metadata.""" + results = [] + if not os.path.isdir(dirpath): + return results + try: + for root, dirs, files in os.walk(dirpath): + # Skip hidden dirs and known noise + dirs[:] = [d for d in dirs if d not in SKIP_PATTERNS and not d.startswith('.')] + for name in sorted(files): + fp = os.path.join(root, name) + if os.path.isfile(fp): + entry = { + 'name': name, + 'md5': md5_file(fp), + 'lines': file_line_count(fp), + 'size_bytes': os.path.getsize(fp), + 'last_modified': last_modified(fp) + } + if relative_to: + entry['relative_path'] = os.path.relpath(fp, relative_to) + results.append(entry) + except (OSError, PermissionError): + pass + return results + + +def scan_skill_dirs(skills_path): + """List skill directories with SKILL.md hash if present.""" + results = [] + if not os.path.isdir(skills_path): + return results + try: + for name in sorted(os.listdir(skills_path)): + skill_dir = os.path.join(skills_path, name) + if os.path.isdir(skill_dir): + entry = { + 'name': name, + 'file_count': sum(1 for _, _, fs in os.walk(skill_dir) for _ in fs), + 'last_modified': dir_last_modified(skill_dir) + } + skill_md = os.path.join(skill_dir, 'SKILL.md') + if os.path.isfile(skill_md): + entry['skill_md_md5'] = md5_file(skill_md) + results.append(entry) + except (OSError, PermissionError): + pass + return results + + +def scan_command_dirs(commands_path, relative_to=None): + """List all command .md files recursively.""" + results = [] + if not os.path.isdir(commands_path): + return results + try: + for root, dirs, files in os.walk(commands_path): + dirs[:] = [d for d in dirs if not d.startswith('.')] + for name in sorted(files): + if name.endswith('.md'): + fp = os.path.join(root, name) + rel = os.path.relpath(fp, commands_path) + entry = { + 'name': rel, + 'md5': md5_file(fp), + 'lines': file_line_count(fp), + 'last_modified': last_modified(fp) + } + results.append(entry) + except (OSError, PermissionError): + pass + return results + + +def parse_settings_json(filepath): + """Parse a settings.json and extract structured data.""" + if not os.path.isfile(filepath): + return None + try: + with open(filepath, 'r') as f: + data = json.load(f) + except (json.JSONDecodeError, OSError): + return {'parse_error': True, 'raw_exists': True} + + result = { + 'exists': True, + 'md5': md5_file(filepath), + 'last_modified': last_modified(filepath) + } + + # Extract hooks + hooks = data.get('hooks', {}) + hook_summary = {} + for event, entries in hooks.items(): + commands = [] + if isinstance(entries, list): + for entry in entries: + if isinstance(entry, dict): + for h in entry.get('hooks', []): + cmd = h.get('command', '') + if cmd: + commands.append(cmd) + elif isinstance(entry, str): + commands.append(entry) + hook_summary[event] = { + 'count': len(commands), + 'commands': commands, + 'is_empty_array': isinstance(entries, list) and len(entries) == 0 + } + result['hooks'] = hook_summary + + # Extract permissions + permissions = data.get('permissions', {}) + result['permissions'] = { + 'allow': permissions.get('allow', []), + 'deny': permissions.get('deny', []) + } + + # Extract MCP servers if present + mcp = data.get('mcpServers', {}) + if mcp: + result['mcp_servers'] = list(mcp.keys()) + + # Extract enabled MCP servers (settings.local.json pattern) + enabled = data.get('enabledMcpjsonServers', []) + if enabled: + result['enabled_mcp_servers'] = enabled + + enable_all = data.get('enableAllProjectMcpServers') + if enable_all is not None: + result['enable_all_project_mcp'] = enable_all + + # Project metadata + project = data.get('project', {}) + if project: + result['project'] = project + + return result + + +def parse_mcp_json(filepath): + """Parse .mcp.json and list all registered server names.""" + if not os.path.isfile(filepath): + return [] + try: + with open(filepath, 'r') as f: + data = json.load(f) + return sorted(data.get('mcpServers', {}).keys()) + except (json.JSONDecodeError, OSError): + return [] + + +def find_git_root(directory): + """Walk up from directory to find the nearest .git root. Returns path or None.""" + current = os.path.abspath(directory) + while True: + if os.path.isdir(os.path.join(current, '.git')): + return current + parent = os.path.dirname(current) + if parent == current: + return None + current = parent + + +def detect_git_boundary(claude_dir, workspace_root): + """Determine which config layers are visible when Claude Code boots in this directory. + + Claude Code resolves the project root from the nearest .git boundary. + - Global ~/.claude/ is always visible + - Workspace root .claude/ is only visible if the project's git root IS the workspace root + - The project's own .claude/ is visible if it's at or under the git root + + Returns a dict describing the visibility context. + """ + # The .claude dir's parent is where Claude Code would boot + parent_dir = os.path.dirname(claude_dir) + git_root = find_git_root(parent_dir) + + ws_abs = os.path.abspath(workspace_root) + has_own_git = git_root is not None and os.path.abspath(git_root) != ws_abs + git_root_rel = os.path.relpath(git_root, ws_abs) if git_root else None + + return { + 'has_own_git': has_own_git, + 'git_root': git_root_rel, + 'sees_global': True, # ~/.claude/ is always visible + 'sees_workspace_root': not has_own_git, # Only if git root == workspace root + 'sees_own_claude': True, # The project's .claude/ is always visible to itself + 'mcp_json_visible': not has_own_git # .mcp.json at workspace root only visible if same git root + } + + +def find_claude_dirs(workspace_root): + """Find all .claude directories recursively, respecting skip patterns.""" + results = [] + seen = set() + for root, dirs, _files in os.walk(workspace_root): + # Filter out skip patterns + dirs[:] = [d for d in dirs if d not in SKIP_PATTERNS] + + if '.claude' in dirs: + claude_path = os.path.join(root, '.claude') + real_path = os.path.realpath(claude_path) + if real_path in seen: + continue + seen.add(real_path) + + rel_path = os.path.relpath(claude_path, workspace_root) + results.append({ + 'absolute_path': claude_path, + 'relative_path': rel_path, + 'parent_dir': os.path.relpath(root, workspace_root) + }) + # Also scan inside .claude for nested .claude dirs (accidental) + nested_claude = os.path.join(claude_path, '.claude') + if os.path.isdir(nested_claude): + nested_real = os.path.realpath(nested_claude) + if nested_real not in seen: + seen.add(nested_real) + nested_rel = os.path.relpath(nested_claude, workspace_root) + results.append({ + 'absolute_path': nested_claude, + 'relative_path': nested_rel, + 'parent_dir': os.path.relpath(claude_path, workspace_root), + 'nested': True + }) + + return results + + +def scan_claude_dir(claude_dir_info, workspace_root): + """Fully scan a single .claude directory.""" + abs_path = claude_dir_info['absolute_path'] + rel_path = claude_dir_info['relative_path'] + + # Git boundary detection + git_context = detect_git_boundary(abs_path, workspace_root) + + entry = { + 'path': rel_path, + 'absolute_path': abs_path, + 'parent': claude_dir_info['parent_dir'], + 'nested': claude_dir_info.get('nested', False), + 'last_modified': dir_last_modified(abs_path), + 'is_template': any(t in rel_path for t in ['_template/', 'template/', 'templates/']), + 'git_boundary': git_context + } + + # Hooks + hooks_dir = os.path.join(abs_path, 'hooks') + entry['hooks'] = scan_files_in_dir(hooks_dir) + + # Commands (recursive — commands can have subdirs) + commands_dir = os.path.join(abs_path, 'commands') + entry['commands'] = scan_command_dirs(commands_dir) + + # Skills + skills_dir = os.path.join(abs_path, 'skills') + entry['skills'] = scan_skill_dirs(skills_dir) + + # Rules + rules_dir = os.path.join(abs_path, 'rules') + entry['rules'] = scan_files_in_dir(rules_dir) + + # Settings + entry['settings_json'] = parse_settings_json(os.path.join(abs_path, 'settings.json')) + entry['settings_local_json'] = parse_settings_json(os.path.join(abs_path, 'settings.local.json')) + + # Other files (top-level only, not in known subdirs) + known_subdirs = {'hooks', 'commands', 'skills', 'rules', 'session-context', 'worktrees'} + other = [] + try: + for name in sorted(os.listdir(abs_path)): + fp = os.path.join(abs_path, name) + if os.path.isfile(fp) and name not in ('settings.json', 'settings.local.json'): + other.append({ + 'name': name, + 'md5': md5_file(fp), + 'size_bytes': os.path.getsize(fp) + }) + elif os.path.isdir(fp) and name not in known_subdirs and name != '.claude': + other.append({ + 'name': name + '/', + 'type': 'directory', + 'file_count': sum(1 for _, _, fs in os.walk(fp) for _ in fs) + }) + except (OSError, PermissionError): + pass + entry['other'] = other + + # Subdirectory presence flags + entry['has_hooks'] = os.path.isdir(hooks_dir) and len(entry['hooks']) > 0 + entry['has_commands'] = os.path.isdir(commands_dir) and len(entry['commands']) > 0 + entry['has_skills'] = os.path.isdir(skills_dir) and len(entry['skills']) > 0 + entry['has_rules'] = os.path.isdir(rules_dir) and len(entry['rules']) > 0 + entry['has_settings'] = entry['settings_json'] is not None + entry['has_settings_local'] = entry['settings_local_json'] is not None + + # Empty directory check + total_items = ( + len(entry['hooks']) + len(entry['commands']) + len(entry['skills']) + + len(entry['rules']) + len(entry['other']) + + (1 if entry['has_settings'] else 0) + + (1 if entry['has_settings_local'] else 0) + ) + entry['is_empty'] = total_items == 0 + + return entry + + +def build_baseline(config_path, label): + """Build a baseline inventory of a .claude config directory.""" + baseline = { + 'path': config_path, + 'label': label, + 'exists': os.path.isdir(config_path) + } + + if not baseline['exists']: + return baseline + + baseline['hooks'] = scan_files_in_dir(os.path.join(config_path, 'hooks')) + baseline['commands'] = scan_command_dirs(os.path.join(config_path, 'commands')) + baseline['skills'] = scan_skill_dirs(os.path.join(config_path, 'skills')) + baseline['settings_json'] = parse_settings_json(os.path.join(config_path, 'settings.json')) + baseline['settings_local_json'] = parse_settings_json(os.path.join(config_path, 'settings.local.json')) + + # Build lookup indexes for fast comparison + baseline['hook_md5_index'] = {h['md5']: h['name'] for h in baseline['hooks'] if h['md5']} + baseline['hook_name_index'] = {h['name']: h['md5'] for h in baseline['hooks'] if h['md5']} + baseline['command_md5_index'] = {c['md5']: c['name'] for c in baseline['commands'] if c['md5']} + baseline['command_name_index'] = {c['name']: c['md5'] for c in baseline['commands'] if c['md5']} + baseline['skill_name_index'] = {s['name']: s.get('skill_md_md5') for s in baseline['skills']} + + return baseline + + +def main(): + parser = argparse.ArgumentParser(description='Scan all .claude/ directories in a workspace') + parser.add_argument('--workspace', default=os.getcwd(), help='Workspace root path') + parser.add_argument('--global-config', default=os.path.join(os.path.expanduser('~'), '.claude'), + help='Global Claude config path') + parser.add_argument('--output', default=None, help='Output JSON path') + args = parser.parse_args() + + workspace = os.path.abspath(args.workspace) + global_config = os.path.abspath(args.global_config) + + # Default output path + if args.output: + output_path = os.path.abspath(args.output) + else: + datasets_dir = os.path.join(workspace, '.base', 'audits', 'data-sets') + os.makedirs(datasets_dir, exist_ok=True) + date_str = datetime.now().strftime('%Y-%m-%d') + output_path = os.path.join(datasets_dir, f'claude-scan-{date_str}.json') + + # Ensure output directory exists + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # Build baselines + global_baseline = build_baseline(global_config, 'global') + workspace_root_claude = os.path.join(workspace, '.claude') + workspace_baseline = build_baseline(workspace_root_claude, 'workspace_root') + + # MCP registry + mcp_servers = parse_mcp_json(os.path.join(workspace, '.mcp.json')) + + # Discover all .claude directories + all_claude_dirs = find_claude_dirs(workspace) + + # Separate root from project-level + project_dirs = [ + d for d in all_claude_dirs + if d['relative_path'] != '.claude' + ] + + # Scan each project-level .claude directory + scanned_directories = [] + for dir_info in project_dirs: + scanned = scan_claude_dir(dir_info, workspace) + scanned_directories.append(scanned) + + # Build the complete dataset + dataset = { + 'meta': { + 'scan_date': datetime.now(tz=timezone.utc).isoformat(), + 'workspace': workspace, + 'global_config': global_config, + 'scanner_version': '1.1.0', + 'total_directories_found': len(all_claude_dirs), + 'project_directories_scanned': len(project_dirs), + 'baseline_directories': 2 + }, + 'baselines': { + 'global': global_baseline, + 'workspace_root': workspace_baseline, + 'mcp_registry': mcp_servers + }, + 'directories': scanned_directories + } + + # Counts summary + total_hooks = sum(len(d['hooks']) for d in scanned_directories) + total_commands = sum(len(d['commands']) for d in scanned_directories) + total_skills = sum(len(d['skills']) for d in scanned_directories) + total_settings = sum(1 for d in scanned_directories if d['has_settings']) + total_settings_local = sum(1 for d in scanned_directories if d['has_settings_local']) + nested_count = sum(1 for d in scanned_directories if d['nested']) + empty_count = sum(1 for d in scanned_directories if d['is_empty']) + template_count = sum(1 for d in scanned_directories if d['is_template']) + own_git_count = sum(1 for d in scanned_directories if d.get('git_boundary', {}).get('has_own_git', False)) + inherits_workspace_count = sum(1 for d in scanned_directories if not d.get('git_boundary', {}).get('has_own_git', False)) + + dataset['summary'] = { + 'total_project_claude_dirs': len(project_dirs), + 'total_hooks': total_hooks, + 'total_commands': total_commands, + 'total_skills': total_skills, + 'total_settings_json': total_settings, + 'total_settings_local_json': total_settings_local, + 'nested_dirs': nested_count, + 'empty_dirs': empty_count, + 'template_dirs': template_count, + 'own_git_boundary': own_git_count, + 'inherits_workspace_root': inherits_workspace_count + } + + # Write output + with open(output_path, 'w') as f: + json.dump(dataset, f, indent=2) + + # Print summary to stdout for the hook/task to capture + print(json.dumps({ + 'status': 'complete', + 'output': output_path, + 'summary': dataset['summary'] + })) + + +if __name__ == '__main__': + main() diff --git a/bin/install.js b/bin/install.js index 4551e06..dbf77d2 100644 --- a/bin/install.js +++ b/bin/install.js @@ -125,19 +125,57 @@ function expandTilde(filePath) { return filePath; } +// Text file suffixes that may contain ${CLAUDE_PLUGIN_ROOT} macro refs. +const TEXT_SUFFIXES = new Set(['.md', '.json', '.js', '.mjs', '.py', '.txt', '.toml', '.yaml', '.yml', '.sh']); + +/** + * Expand ${CLAUDE_PLUGIN_ROOT} in a text file during npx copy. + * When install.js runs via npx, CLAUDE_PLUGIN_ROOT is not set; the macro + * is replaced with the actual install target so no literal placeholder + * remains in the installed output. Idempotent: files without the macro are + * copied byte-for-byte (the Buffer fast-path below detects that case). + */ +function copyFileExpandingMacro(srcPath, destPath, pluginRoot) { + const ext = path.extname(srcPath).toLowerCase(); + if (!pluginRoot || (!TEXT_SUFFIXES.has(ext) && ext !== '')) { + // Non-text or no macro expansion needed — byte-for-byte copy. + fs.copyFileSync(srcPath, destPath); + return; + } + let content; + try { + content = fs.readFileSync(srcPath, 'utf8'); + } catch { + // Binary read failed — fall back to byte copy. + fs.copyFileSync(srcPath, destPath); + return; + } + if (!content.includes('${CLAUDE_PLUGIN_ROOT}')) { + // Fast path: no macro present — write as-is. + fs.writeFileSync(destPath, content, 'utf8'); + return; + } + // Replace ALL occurrences of ${CLAUDE_PLUGIN_ROOT} with the real path. + const expanded = content.split('${CLAUDE_PLUGIN_ROOT}').join(pluginRoot); + fs.writeFileSync(destPath, expanded, 'utf8'); +} + /** - * Recursively copy directory + * Recursively copy directory, expanding ${CLAUDE_PLUGIN_ROOT} in text files. + * pluginRoot is the resolved install target (e.g. ~/.claude or ./.claude) + * so that any plugin-native refs in the source are grounded to real paths + * in the npx-installed output. */ -function copyDir(srcDir, destDir) { +function copyDir(srcDir, destDir, pluginRoot) { fs.mkdirSync(destDir, { recursive: true }); const entries = fs.readdirSync(srcDir, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(srcDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { - copyDir(srcPath, destPath); + copyDir(srcPath, destPath, pluginRoot); } else { - fs.copyFileSync(srcPath, destPath); + copyFileExpandingMacro(srcPath, destPath, pluginRoot); } } } diff --git a/commands/audit-claude-md.md b/commands/audit-claude-md.md new file mode 100644 index 0000000..7bbd775 --- /dev/null +++ b/commands/audit-claude-md.md @@ -0,0 +1,44 @@ +--- +name: audit-claude-md +description: Audit CLAUDE.md against the CLAUDE.md Strategy and generate a compliant version +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash] +--- + + +Audit the project's CLAUDE.md for strategy compliance, interactively rewrite it section by section, and route operational rules to CARL or an artifact. + +**When to use:** "audit claude md", "check my claude.md", "rewrite my claude.md", after major workspace changes. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/claudemd-strategy.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/claudemd-template.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit-claude-md.md + + + +$ARGUMENTS + +@CLAUDE.md + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit-claude-md.md + +Key gates (do NOT skip): +1. Load strategy + template BEFORE reading user's CLAUDE.md +2. Present full audit classification — wait for user approval +3. Detect CARL — wait for user decision on rule routing +4. Propose each section individually — wait for approval per section +5. Write to CLAUDE.base.md (never overwrite CLAUDE.md) + + + +- [ ] Strategy framework loaded first +- [ ] Every line classified (KEEP/REMOVE/RESTRUCTURE/CARL_CANDIDATE) +- [ ] User approved audit before rewriting began +- [ ] CARL detection completed, rule routing decided +- [ ] Each section approved individually +- [ ] Final CLAUDE.base.md under 100 lines +- [ ] Original CLAUDE.md untouched + diff --git a/commands/audit-claude.md b/commands/audit-claude.md new file mode 100644 index 0000000..96d05f4 --- /dev/null +++ b/commands/audit-claude.md @@ -0,0 +1,45 @@ +--- +name: audit-claude +description: Audit .claude/ directories across workspace for sprawl, duplication, and misalignment +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] +--- + + +Audit all .claude/ directories in this workspace. Discover sprawl, classify items against the global/workspace/project hierarchy, and remediate with operator approval at every step. + +**When to use:** "audit claude config", "clean up .claude dirs", "check claude setup", after installing global tools. + + + +@base-framework/tasks/audit-claude.md +@base-framework/frameworks/claude-config-alignment.md + + + +$ARGUMENTS + +Global config: ~/.claude/ +Workspace root config: .claude/ + + + +Follow task: @base-framework/tasks/audit-claude.md + +The framework file defines classification rules, safety protocol, and output format. +The task file defines the step-by-step process. + +Key principles: +- Every change requires operator approval +- Process from lowest risk to highest +- Never batch-delete without per-item confirmation +- Explain what, why, and what could go wrong for every change +- Copy-then-verify-then-remove, never move + + + +- [ ] All .claude/ directories discovered and inventoried +- [ ] Items classified with evidence +- [ ] Operator confirmed classifications +- [ ] Remediation executed by risk group with verification +- [ ] Summary report with manual follow-up items + diff --git a/commands/audit.md b/commands/audit.md new file mode 100644 index 0000000..2c24f9f --- /dev/null +++ b/commands/audit.md @@ -0,0 +1,33 @@ +--- +name: audit +description: Deep workspace optimization +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion] +--- + + +Deep workspace audit — comprehensive optimization across all areas with actionable recommendations. + +**When to use:** "audit my workspace", "deep clean", monthly optimization. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/context/base-principles.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/audit-strategies.md + + + +$ARGUMENTS + +@.base/workspace.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit.md + + + +- [ ] All areas audited with strategy-specific checks +- [ ] Findings categorized and prioritized +- [ ] Actionable recommendations presented + diff --git a/commands/carl-hygiene.md b/commands/carl-hygiene.md new file mode 100644 index 0000000..16fec56 --- /dev/null +++ b/commands/carl-hygiene.md @@ -0,0 +1,33 @@ +--- +name: carl-hygiene +description: CARL domain maintenance and rule review +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, carl_v2_list_domains, carl_v2_get_domain, carl_v2_get_staged, carl_v2_approve_proposal, carl_v2_remove_rule, carl_v2_replace_rules, carl_v2_archive_decision] +--- + + +CARL rule lifecycle management — review staleness, staging pipeline, domain health. + +**When to use:** "carl hygiene", "review carl rules", "clean up carl". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/carl-hygiene.md + + + +$ARGUMENTS + +@.carl/carl.json +@.base/workspace.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/carl-hygiene.md + + + +- [ ] All domains reviewed for staleness +- [ ] Duplicate/conflicting rules identified +- [ ] Staging pipeline processed +- [ ] carl_hygiene.last_run updated in workspace.json + diff --git a/commands/groom.md b/commands/groom.md new file mode 100644 index 0000000..ab2fb3e --- /dev/null +++ b/commands/groom.md @@ -0,0 +1,35 @@ +--- +name: groom +description: Weekly workspace maintenance cycle +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] +--- + + +Structured weekly maintenance — review each workspace area, update statuses, archive stale items, reduce drift. + +**When to use:** Weekly maintenance, "groom my workspace", "run grooming". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/groom.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/context/base-principles.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/audit-strategies.md + + + +$ARGUMENTS + +@.base/workspace.json +@.base/data/state.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/groom.md + + + +- [ ] All workspace areas reviewed +- [ ] Stale items addressed +- [ ] state.json updated with groom results +- [ ] Drift score recalculated + diff --git a/commands/history.md b/commands/history.md new file mode 100644 index 0000000..b06c773 --- /dev/null +++ b/commands/history.md @@ -0,0 +1,27 @@ +--- +name: history +description: Workspace evolution timeline +allowed-tools: [Read, Glob, Bash] +--- + + +Show workspace evolution — grooming history, audits, major changes over time. + +**When to use:** "workspace history", "show evolution", "what's changed". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/history.md + + + +@.base/data/state.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/history.md + + + +- [ ] Timeline of workspace events displayed + diff --git a/commands/orientation.md b/commands/orientation.md new file mode 100644 index 0000000..76e1be8 --- /dev/null +++ b/commands/orientation.md @@ -0,0 +1,87 @@ + +## What +Guided operator identity workflow that produces or updates `.base/operator.json`. Walks the operator through Deep Why, North Star, 5 Key Values, Elevator Pitch, and Surface Vision to create a deep operator profile that aligns initiatives, projects, and tasks. + +## When to Use +- First time setting up a workspace (no operator.json exists) +- Operator feels disoriented and needs to realign +- Periodic review of identity anchors (quarterly recommended) +- After major life or business shifts + +## Not For +- Project-level planning (use /paul:plan) +- Task tracking updates (use Apex MCP directly) +- Workspace health checks (use /base:pulse) + + + +## Role +Orientation guide — facilitates deep self-inquiry without rushing, fluff, or false depth. Holds space for reflection while keeping momentum. + +## Style +- Direct questions, no leading preamble +- Waits between phases — never auto-advances +- Uses [N] option brackets for navigation at every decision point +- Reflects back what the operator said before synthesizing — no assumptions +- Challenges surface-level answers with "go deeper" follow-ups when warranted + +## Expertise +- Identity frameworks, values clarification, vision anchoring +- Composable workflow orchestration (parent/child task flow) +- Operator profile data modeling + + + +| Command | Description | Routes To | +|---------|-------------|-----------| +| `/base:orientation` | Full orientation workflow | (this entry point) | + + + +## Always Load +Nothing — lightweight until invoked. + +## Load on Command +@orientation/tasks/new-orientation.md (when no operator.json exists) +@orientation/tasks/reorientation.md (when operator.json exists and operator wants to reset) + +## Load on Demand +@orientation/tasks/deep-why.md (Phase 1 of orientation) +@orientation/tasks/north-star.md (Phase 2 of orientation) +@orientation/tasks/key-values.md (Phase 3 of orientation) +@orientation/tasks/elevator-pitch.md (Phase 4 of orientation) +@orientation/tasks/surface-vision.md (Phase 5 of orientation) +@orientation/tasks/initiatives.md (Phase 6 of orientation) +@orientation/tasks/project-mapping.md (Phase 7 of orientation) +@orientation/tasks/task-seeding.md (Phase 8 of orientation) +@orientation/templates/operator-json.md (schema reference for operator.json) + + + +## Orientation Check + +Read `.base/operator.json` to determine current state. + +**If operator.json does NOT exist:** +> No operator profile found. This is your first orientation. +> +> This workflow will walk you through 8 phases — 5 identity exercises then 3 workspace alignment steps. Defines who you are, where you're headed, and organizes your work to match. Takes 30-60 minutes depending on how deep you go. +> +> **[1] Begin orientation** +> **[2] Not now — exit** + +If [1]: Load and execute @orientation/tasks/new-orientation.md + +**If operator.json EXISTS:** +Load and display current profile summary (North Star, Deep Why, Values, Pitch, Vision — one line each). + +> Your last orientation was on {last_updated date}. +> +> **[1] Full reorientation** — reset everything from scratch +> **[2] Update a specific section** — keep the rest +> **[3] Review only** — just look, don't change anything + +If [1]: Load and execute @orientation/tasks/reorientation.md +If [2]: Ask which section, then load that specific task +If [3]: Display full profile and exit + diff --git a/commands/orientation/tasks/deep-why.md b/commands/orientation/tasks/deep-why.md new file mode 100644 index 0000000..1aac34c --- /dev/null +++ b/commands/orientation/tasks/deep-why.md @@ -0,0 +1,132 @@ + +Excavate the operator's root motivation through a 5-layer "but why?" process. Each layer peels back a surface answer to find the one beneath it, arriving at a foundational Deep Why statement. + + + +As an operator, I want to uncover my deepest motivation for doing what I do, so that my work connects to something real and sustaining rather than external pressure or habit. + + + +- Phase 1 of new orientation +- Operator chose to reorient their Deep Why specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +> Your previous Deep Why was: +> *"{previous statement}"* +> +> We're going to excavate fresh. You can land in the same place or somewhere new. Either is fine. + + +> **Layer 1 of 5** +> +> Why do you do what you do? Not the business answer. Not the resume answer. The real one. + +**Wait for response.** + + + +Reflect back what the operator said in Layer 1 — one sentence, their words not yours. + +> You said: "{reflection}" +> +> **Layer 2 of 5** +> +> But why does that matter to you? + +**Wait for response.** + + + +Reflect back Layer 2. + +> You said: "{reflection}" +> +> **Layer 3 of 5** +> +> But why? + +If the answer feels surface-level or performative, push: "That sounds like something you'd say to someone else. What's the version you'd say to yourself at 2am?" + +**Wait for response.** + + + +Reflect back Layer 3. + +> You said: "{reflection}" +> +> **Layer 4 of 5** +> +> Why does that drive you more than anything else? + +**Wait for response.** + + + +Reflect back Layer 4. + +> You said: "{reflection}" +> +> **Layer 5 of 5 — the root** +> +> If everything else was stripped away — the business, the tools, the audience — what's left? What's the thing that would still make you get up and build? + +**Wait for response.** + + + +Now synthesize all 5 layers into a single Deep Why statement. + +1. Read back all 5 layers +2. Draft a one-sentence synthesis that captures the root, not just Layer 5 but the thread through all layers +3. Present it: + +> Here are your 5 layers: +> 1. {layer 1} +> 2. {layer 2} +> 3. {layer 3} +> 4. {layer 4} +> 5. {layer 5} +> +> **Your Deep Why:** +> *"{synthesized statement}"* +> +> **[1] That's it** — lock it in +> **[2] Close but needs tweaking** — let me adjust the wording +> **[3] Not right** — let me redo a layer + +If [2]: Let operator edit the statement directly. +If [3]: Ask which layer to redo, then resume from that layer. + +**Wait for response.** + + + +Deep Why is locked. + +Return to parent workflow with: +- `layers`: array of 5 layer answers +- `statement`: final approved synthesis +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Deep Why data: 5 layers + synthesized statement, ready for operator.json. + + + +- [ ] All 5 layers answered by operator (not generated) +- [ ] Each layer reflected back before asking the next +- [ ] Surface answers challenged when detected +- [ ] Synthesis captures the thread, not just layer 5 +- [ ] Operator approved the final statement + diff --git a/commands/orientation/tasks/elevator-pitch.md b/commands/orientation/tasks/elevator-pitch.md new file mode 100644 index 0000000..41dc5c9 --- /dev/null +++ b/commands/orientation/tasks/elevator-pitch.md @@ -0,0 +1,115 @@ + +Synthesize the operator's identity into a 4-floor elevator pitch — a 30-second statement of who they are, what they do, why it matters, and where they're going. Built on top of Deep Why, North Star, and Values. + + + +As an operator, I want a clear, statable pitch for who I am and what I'm about, so that I can articulate my value to anyone in 30 seconds without fumbling. + + + +- Phase 4 of new orientation (after Key Values) +- Operator chose to reorient their Elevator Pitch specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +> Your previous pitch was: +> *"{previous pitch}"* + + +> Your foundation so far: +> - **Deep Why:** {statement} +> - **North Star:** {metric} +> - **Values:** {v1}, {v2}, {v3}, {v4}, {v5} +> +> An elevator pitch has 4 floors. Each floor is one sentence: +> +> **Floor 1** — Who you are (identity, not job title) +> **Floor 2** — What you do (the work, in plain language) +> **Floor 3** — Why it matters (the impact) +> **Floor 4** — What's next (the vision or the ask) +> +> Let's build each floor. Don't try to be clever — try to be clear. + + + +> **Floor 1: Who are you?** +> +> Not your job title. Not your company. If you met someone at a party and they asked "what are you about?" — what's the one-sentence answer? + +**Wait for response.** + + + +Reflect Floor 1 back, then: + +> **Floor 2: What do you do?** +> +> The work itself. What does your day look like when it's going well? One sentence. + +**Wait for response.** + + + +Reflect Floor 2 back, then: + +> **Floor 3: Why does it matter?** +> +> Who benefits and how? This connects to your Deep Why. One sentence. + +**Wait for response.** + + + +Reflect Floor 3 back, then: + +> **Floor 4: What's next?** +> +> Where is this going? What's the thing you're building toward? This connects to your North Star. One sentence. + +**Wait for response.** + + + +Assemble the full pitch from all 4 floors. Read it as one continuous statement. + +> ## Your Elevator Pitch +> +> *"{floor1} {floor2} {floor3} {floor4}"* +> +> Read it out loud. Does it sound like you? +> +> **[1] That's me — lock it in** +> **[2] Reword a floor** — tell me which one +> **[3] Start over** — the whole thing feels off + +**Wait for response.** + + + +Elevator Pitch is locked. + +Return to parent workflow with: +- `pitch`: full assembled pitch +- `floors`: object with floor_1 through floor_4 +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Elevator Pitch data: 4 floors + assembled pitch, ready for operator.json. + + + +- [ ] Each floor answered individually by operator +- [ ] Full pitch reads as one coherent 30-second statement +- [ ] Floor 3 connects to Deep Why +- [ ] Floor 4 connects to North Star +- [ ] Operator confirmed it sounds like them + diff --git a/commands/orientation/tasks/initiatives.md b/commands/orientation/tasks/initiatives.md new file mode 100644 index 0000000..d3cd31a --- /dev/null +++ b/commands/orientation/tasks/initiatives.md @@ -0,0 +1,98 @@ + +Guide the operator through defining or reviewing goal-oriented initiatives aligned to their North Star. Each initiative is a measurable strategic objective, not an entity or project. + + + +As an operator who has completed their identity profile, I want to define the strategic objectives my work serves, so that every project and task traces back to a clear goal. + + + +- Phase 6 of new orientation (after Surface Vision) +- Operator chose to reorient their initiatives specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +Pull current initiatives via base_list_projects(type="initiative") and display them: + +> ## Current Initiatives +> +> {For each initiative: title, description, status, project count} +> +> We'll review each one against your North Star. + + + +> Your North Star: *"{north_star_metric}"* +> Your Deep Why: *"{deep_why_statement}"* +> +> Initiatives are the strategic objectives that move you toward the North Star. NEVER confuse them with businesses or entities — an initiative is a measurable goal. +> +> Example: Not "C&C Strategic Consulting" but "Build C&C to $7k/month MRR" +> +> What are the 2-4 major objectives you're working toward right now? Think in terms of outcomes, not labels. + + +**Wait for response.** + + + +For each initiative the operator names: + +> **Initiative: "{title}"** +> +> 1. How does this connect to your North Star? +> 2. What's the key metric or success criteria? +> 3. Is there a timeframe? + +Capture: title, description, priority, category, metric, timeframe. + +NEVER let an initiative through that is actually an entity (person, business, org). Push back: "That's a business name, not a goal. What's the measurable objective for that business?" + +**Wait for response between each initiative.** + + + +Present all initiatives: + +> ## Your Initiatives +> +> 1. **{title}** — {description} ({metric}, {timeframe}) +> 2. **{title}** — {description} ({metric}, {timeframe}) +> ... +> +> **[1] Lock these in** +> **[2] Add another** +> **[3] Edit one** +> **[4] Remove one** + +When locked, write each initiative via base_add_project(type="initiative") with full metadata. + +**Wait for response.** + + + +Initiatives are locked. + +Return to parent workflow with: +- `initiatives`: array of created initiative IDs and titles +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Initiatives created in Apex via MCP. Each has title, description, priority, category, metric alignment to North Star. + + + +- [ ] Each initiative is a measurable goal, not an entity +- [ ] Each initiative connects to the North Star +- [ ] Written via base_add_project MCP, not manual file edits +- [ ] Operator approved the final set + diff --git a/commands/orientation/tasks/key-values.md b/commands/orientation/tasks/key-values.md new file mode 100644 index 0000000..cb4a634 --- /dev/null +++ b/commands/orientation/tasks/key-values.md @@ -0,0 +1,130 @@ + +Guide the operator through identifying and ranking their top 5 personal values — the core principles they operate by. The constraint of exactly 5 forces prioritization and clarity. + + + +As an operator, I want to be clear on my top 5 operating principles, so that I can make decisions faster and stay aligned when things get chaotic. + + + +- Phase 3 of new orientation (after North Star) +- Operator chose to reorient their Key Values specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +> Your previous values were: +> 1. {v1} — {meaning1} +> 2. {v2} — {meaning2} +> 3. {v3} — {meaning3} +> 4. {v4} — {meaning4} +> 5. {v5} — {meaning5} + + +> Your Deep Why: *"{deep_why_statement}"* +> Your North Star: *"{north_star_metric}"* +> +> Values are the principles you actually operate by — not aspirational ones you wish you had. Think about the last time you made a hard decision. What did you lean on? +> +> Give me 7-10 values that feel true. Don't overthink it — we'll narrow down. + +**Wait for response.** + + + +Before cutting, reflect each value back with a personalized one-line description based on what you know about the operator. This helps them see what each value actually means to them, not just the word. + +> Here's what I'm hearing from you: +> +> 1. **{Value}** — {personalized description based on context} +> 2. **{Value}** — {personalized description} +> ... +> +> Is anything missing that you'd fight for if challenged? Or are these the ones? +> +> **[1] These are the ones — let's cut to 5** +> **[2] I want to add 1-2 more, then cut** + +**Wait for response.** + + + +Force the cut. ALWAYS show the personalized description alongside each value — never list bare value names. + +> Now the hard part — cut to 5. The ones that survive are the ones you'd defend if someone challenged them. +> +> Which ones can you live without? Drop them one at a time until you're at 5. + +If operator struggles, offer a forcing question: "If you could only teach your kids 5 principles about how to live, which of these make the cut?" + +**Wait for response.** + + + +Present values WITH their descriptions, then ask for ranking: + +> 1. **{Value}** — {description} +> 2. **{Value}** — {description} +> ... +> +> Now rank them 1-5. #1 is the one that wins when two values conflict. + +**Wait for response.** + + + +For each value in ranked order: + +> **{Value #N}: {value name}** +> In one sentence — what does this look like in practice for you? Not the dictionary definition. How does this value show up in your daily decisions? + +Iterate through all 5, waiting for each response. + +**Wait for each response.** + + + +Present the complete values list: + +> ## Your 5 Key Values +> +> 1. **{v1}** — {meaning} +> 2. **{v2}** — {meaning} +> 3. **{v3}** — {meaning} +> 4. **{v4}** — {meaning} +> 5. **{v5}** — {meaning} +> +> **[1] Lock it in** +> **[2] Swap a value** +> **[3] Rerank** +> **[4] Reword a meaning** + +**Wait for response.** + + + +Key Values are locked. + +Return to parent workflow with: +- `values`: array of 5 objects (rank, value, meaning) +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Key Values data: 5 ranked values with practical meanings, ready for operator.json. + + + +- [ ] Started with 7-10 brainstormed values +- [ ] Narrowed to exactly 5 +- [ ] Ranked 1-5 with explicit prioritization +- [ ] Each value has a practical meaning (not dictionary definition) +- [ ] Operator approved the final list + diff --git a/commands/orientation/tasks/new-orientation.md b/commands/orientation/tasks/new-orientation.md new file mode 100644 index 0000000..8553ae6 --- /dev/null +++ b/commands/orientation/tasks/new-orientation.md @@ -0,0 +1,162 @@ + +Orchestrate a full first-time orientation through all 5 phases: Deep Why, North Star, Key Values, Elevator Pitch, Surface Vision. Composes each phase task sequentially, passing context forward, and produces operator.json at the end. + + + +As an operator setting up my workspace for the first time, I want a guided walkthrough of my identity anchors, so that my initiatives, projects, and tasks align to who I am and where I'm going. + + + +- No operator.json exists yet +- Entry point routes here when operator chooses [1] Begin orientation + + + +@../templates/operator-json.md + + + + + +Brief the operator on what's coming. + +> You'll go through 5 exercises. Each builds on the last: +> +> 1. **Deep Why** — excavate your root motivation (5 layers) +> 2. **North Star** — define the one metric everything points at +> 3. **Key Values** — narrow to your top 5 operating principles +> 4. **Elevator Pitch** — synthesize who you are in 30 seconds +> 5. **Surface Vision** — anchor a tangible picture of the future +> +> Take your time. There's no wrong answers, but surface-level ones won't serve you. +> +> **[1] Let's go — start with Deep Why** +> **[2] I need a minute — come back to this** + +If [2]: Exit gracefully. Orientation can resume anytime via `/base:orientation`. + +**Wait for response.** + + + +Load and execute @deep-why.md + +Pass no prior context (this is the first phase). + +When deep-why.md completes and locks its result, capture the output (layers + statement) and continue here. + + + +Load and execute @north-star.md + +Pass context: the Deep Why statement from Phase 1. + +When north-star.md completes and locks its result, capture the output (metric + timeframe + rationale) and continue here. + + + +Load and execute @key-values.md + +Pass context: Deep Why statement + North Star metric. + +When key-values.md completes and locks its result, capture the output (5 ranked values with meanings) and continue here. + + + +Load and execute @elevator-pitch.md + +Pass context: Deep Why statement + North Star metric + Key Values list. + +When elevator-pitch.md completes and locks its result, capture the output (4 floors + full pitch) and continue here. + + + +Load and execute @surface-vision.md + +Pass context: Deep Why statement + North Star metric + Key Values + Elevator Pitch. + +When surface-vision.md completes and locks its result, capture the output (scenes + summary) and continue here. + + + +All 5 phases complete. Synthesize into operator.json. + +1. Read @../templates/operator-json.md for schema +2. Populate all fields from captured phase outputs +3. Write to `.base/operator.json` +4. Display the complete profile in a clean summary: + +> ## Your Operator Profile +> +> **Deep Why:** {statement} +> **North Star:** {metric} ({timeframe}) +> **Values:** {value1}, {value2}, {value3}, {value4}, {value5} +> **Elevator Pitch:** {full pitch} +> **Surface Vision:** {summary} +> +> Profile saved to `.base/operator.json`. +> +> **[1] Looks right — done** +> **[2] Something's off — let me adjust a section** + +If [2]: Ask which section, load that specific task for revision, then re-write operator.json. + +**Wait for response.** + + + +Load and execute @initiatives.md + +Pass context: Full operator profile (Deep Why, North Star, Values). + +When initiatives.md completes and locks its result, capture the output (initiative IDs and titles) and continue here. + + + +Load and execute @project-mapping.md + +Pass context: Created initiatives from Phase 6. + +When project-mapping.md completes and locks its result, capture the output (mapping counts, PAUL sync counts) and continue here. + + + +Load and execute @task-seeding.md + +Pass context: Initiative → Project mapping from Phase 7. + +When task-seeding.md completes and locks its result, capture the output (task counts) and continue here. + + + +All 8 phases complete. Display final summary: + +> ## Orientation Complete +> +> **Operator Profile:** `.base/operator.json` +> - Deep Why, North Star, Values, Pitch, Vision — locked +> +> **Initiatives:** {count} defined, aligned to North Star +> **Projects:** {mapped_count} mapped to initiatives, {unparented_count} unparented +> **PAUL Satellites:** {paul_synced} synced +> **Tasks:** {tasks_created} seeded across {projects_with_tasks} projects +> +> You're oriented. Run `/base:orientation` anytime to review or reorient. + + + + + +Complete workspace orientation: operator.json populated, initiatives defined, projects mapped, PAUL synced, tasks seeded. + + + +- [ ] All 8 phase tasks executed in order +- [ ] Each phase locked before advancing to next +- [ ] Context passed forward between phases +- [ ] operator.json written with all fields populated +- [ ] Initiatives created via Apex MCP +- [ ] Projects mapped to initiatives with PAUL data synced +- [ ] Tasks seeded under projects +- [ ] Operator reviewed and approved at each phase + diff --git a/commands/orientation/tasks/north-star.md b/commands/orientation/tasks/north-star.md new file mode 100644 index 0000000..ebef59b --- /dev/null +++ b/commands/orientation/tasks/north-star.md @@ -0,0 +1,97 @@ + +Define the operator's North Star — the one key metric or outcome that everything else aligns toward. Derives from the Deep Why and gives initiatives a measurable target. + + + +As an operator, I want a single guiding metric that tells me whether my work is pointing in the right direction, so that I can evaluate opportunities and say no to misaligned ones. + + + +- Phase 2 of new orientation (after Deep Why) +- Operator chose to reorient their North Star specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +> Your previous North Star was: +> *"{previous metric}" ({previous timeframe})* + + +> Your Deep Why: *"{deep_why_statement}"* +> +> A North Star is the one metric or outcome that, if you achieved it, would mean your Deep Why is being lived. It's not a task. It's not a project. It's the thing that makes all the projects make sense. +> +> What's the one outcome that matters most to you right now? Think in terms of something you could measure or clearly evaluate. + +**Wait for response.** + + + +Evaluate the response: + +- If too vague ("be successful", "make an impact"): Push for specificity. "What would you point to as proof that's happening?" +- If too narrow ("launch CaseGate"): Push for altitude. "That's a project, not a star. What does completing that project serve?" +- If it's a feeling ("feel free"): Acknowledge it, then ask "What would be true in your life when you feel that? What's the measurable version?" + +Once the metric is crisp: + +> **Your North Star metric:** +> *"{metric}"* +> +> What timeframe feels right for this? Not a deadline — a horizon. When would you evaluate whether you're on track? +> +> **[1] 6 months** +> **[2] 1 year** +> **[3] 2-3 years** +> **[4] Custom — I'll specify** + +**Wait for response.** + + + +> Last piece — why this metric above all the others you could have chosen? One sentence. + +**Wait for response.** + + + +> **Your North Star:** +> *"{metric}"* +> **Timeframe:** {timeframe} +> **Why this one:** {rationale} +> +> **[1] Lock it in** +> **[2] Adjust the metric** +> **[3] Adjust the timeframe** + +**Wait for response.** + + + +North Star is locked. + +Return to parent workflow with: +- `metric`: the north star metric +- `timeframe`: chosen horizon +- `rationale`: why this metric +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +North Star data: metric + timeframe + rationale, ready for operator.json. + + + +- [ ] Metric is specific and evaluable (not vague aspiration) +- [ ] Metric connects back to Deep Why +- [ ] Timeframe is set +- [ ] Rationale captured +- [ ] Operator approved the final North Star + diff --git a/commands/orientation/tasks/project-mapping.md b/commands/orientation/tasks/project-mapping.md new file mode 100644 index 0000000..8744cb2 --- /dev/null +++ b/commands/orientation/tasks/project-mapping.md @@ -0,0 +1,103 @@ + +Map existing projects to their parent initiatives and sync PAUL satellite data. Every project should trace to an initiative or be explicitly unparented. + + + +As an operator with defined initiatives, I want my projects organized under the right strategic objectives with current PAUL data, so that I can see which work serves which goal. + + + +- Phase 7 of new orientation (after Initiatives) +- Operator wants to reorganize project-to-initiative mappings +- Composed by new-orientation.md or reorientation.md + + + + + +Pull initiatives and projects in parallel: +- base_list_projects(type="initiative") +- base_list_projects(type="project") + +Display initiatives as target buckets, then list all projects with their current parent_id (or "unparented"). + +> ## Initiatives (target buckets) +> {For each: ID, title} +> +> ## Projects to Map +> {For each: ID, title, current parent, status, PAUL satellite if any} +> +> I'll go through each project. Tell me which initiative it belongs under, or say "none" to leave it unparented. + + + +For each project without a parent (or with a stale/wrong parent): + +> **{PRJ-ID}: {title}** ({status}) +> {PAUL: satellite_name, phase if applicable} +> +> Which initiative does this serve? +> {List initiative options as [N] brackets} +> **[N+1] None — leave unparented** +> **[N+2] Archive — no longer active** + +**Wait for response before moving to next project.** + +When assigned, update via base_update_project(id, {parent_id: "INI-XXX"}). + + + +For each project that has a PAUL satellite (paul.json exists in its location): + +1. Read the paul.json file from the project's location +2. Update the project's paul field with current: satellite_name, location, milestone, phase, loop_position, handoff status and path +3. Update via base_update_project + +Report: "{N} PAUL satellites synced" + + + +Display the final mapping: + +> ## Initiative → Project Mapping +> +> **{INI-001}: {title}** +> - {PRJ-XXX}: {title} (PAUL: {phase} | {status}) +> - ... +> +> **{INI-002}: {title}** +> - ... +> +> **Unparented:** +> - {PRJ-XXX}: {title} +> +> **[1] Looks right — lock it in** +> **[2] Move a project** + +**Wait for response.** + + + +Project mapping is locked. + +Return to parent workflow with: +- `mapped_count`: number of projects assigned to initiatives +- `unparented_count`: number left without a parent +- `paul_synced`: number of PAUL satellites updated +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +All projects mapped to initiatives via Apex MCP. PAUL satellite data synced from paul.json files. + + + +- [ ] Every project reviewed — assigned to initiative, left unparented, or archived +- [ ] Parent IDs set via base_update_project MCP +- [ ] PAUL satellites synced from source paul.json files +- [ ] Final mapping displayed and approved by operator + diff --git a/commands/orientation/tasks/reorientation.md b/commands/orientation/tasks/reorientation.md new file mode 100644 index 0000000..d6ed4e5 --- /dev/null +++ b/commands/orientation/tasks/reorientation.md @@ -0,0 +1,96 @@ + +Guide an operator through reorientation when an existing operator.json is present. Iterates through each section, showing current values and prompting for keep/reset decisions before composing relevant phase tasks. + + + +As an operator who has been through orientation before, I want to selectively reset parts of my identity profile, so that my profile evolves with me without starting completely from scratch every time. + + + +- operator.json already exists +- Operator chose [1] Full reorientation from entry point + + + +@../templates/operator-json.md + + + + + +Read `.base/operator.json` and display the full current profile. + +> ## Current Operator Profile +> +> **Deep Why:** {current statement} +> *(Last set: {date})* +> +> **North Star:** {current metric} ({timeframe}) +> *(Last set: {date})* +> +> **Values:** {v1}, {v2}, {v3}, {v4}, {v5} +> *(Last set: {date})* +> +> **Elevator Pitch:** {current pitch} +> *(Last set: {date})* +> +> **Surface Vision:** {current summary} +> *(Last set: {date})* + +Then begin iterating through each section. + + + +For each section in order (Deep Why, North Star, Key Values, Elevator Pitch, Surface Vision, Initiatives, Project Mapping, Task Seeding): + +> **{Section Name}** +> Current: {current value — one-line summary} +> Last set: {date} +> +> **[1] Keep** — this still resonates +> **[2] Reorient** — this needs work +> **[3] Skip for now** — come back to it later + +**Wait for response before moving to next section.** + +If [2]: Load and execute the corresponding phase task (e.g., @deep-why.md for Deep Why, @initiatives.md for Initiatives, @project-mapping.md for Project Mapping, @task-seeding.md for Task Seeding), passing current value as "previous orientation" context so the operator can see what they had before. When phase task completes, capture new output and continue iteration. + +If [1] or [3]: Keep current value, move to next section. + +Track which sections were reoriented vs kept. + + + +After iterating all 8 sections: + +1. Merge kept values with new values from reoriented sections +2. Update `last_updated` timestamp +3. Update `completed_at` only for sections that were reoriented +4. Write updated `.base/operator.json` +5. Display summary showing what changed: + +> ## Reorientation Complete +> +> **Changed:** {list of reoriented sections} +> **Kept:** {list of kept sections} +> **Skipped:** {list of skipped sections} +> +> Profile updated at `.base/operator.json`. + +**Wait for acknowledgment.** + + + + + +Updated `.base/operator.json` with selectively reoriented sections. + + + +- [ ] Current profile loaded and displayed +- [ ] Each section presented with keep/reorient/skip options +- [ ] Reoriented sections went through their full phase task +- [ ] Kept sections preserved unchanged +- [ ] operator.json updated with merged results +- [ ] Summary shows what changed vs what stayed + diff --git a/commands/orientation/tasks/surface-vision.md b/commands/orientation/tasks/surface-vision.md new file mode 100644 index 0000000..3235ca6 --- /dev/null +++ b/commands/orientation/tasks/surface-vision.md @@ -0,0 +1,113 @@ + +Guide the operator to conjure 2-5 concrete, tangible future moments that represent what their life looks like when things are working. These are sensory anchors — specific enough to visualize, "superficial" on purpose because the surface is what the inner world connects to. + + + +As an operator, I want tangible anchor points for the future I'm building toward, so that my inner psychology has something concrete to orient around rather than abstract goals. + + + +- Phase 5 of new orientation (after Elevator Pitch) +- Operator chose to reorient their Surface Vision specifically +- Composed by new-orientation.md or reorientation.md + + + + + + +> Your previous Surface Vision scenes: +> - {scene 1} +> - {scene 2} +> - {scene 3} +> Summary: *"{previous summary}"* + + +> Your foundation: +> - **Deep Why:** {statement} +> - **North Star:** {metric} +> - **Values:** {v1}, {v2}, {v3}, {v4}, {v5} +> - **Pitch:** {pitch} +> +> Surface Vision is different from the others. This isn't about metrics or statements. This is about moments. +> +> Close your eyes for a second. Picture your life when the North Star is hit and the Deep Why is being lived daily. Don't think about the business model or the revenue. Think about a single moment in a regular day. +> +> What does one specific moment look like? Be concrete — where are you, what are you doing, what do you see, hear, feel? + +**Wait for response.** + + + +After the first scene: + +> Good. That's Scene 1. +> +> Give me another moment. Different context — maybe a different time of day, different setting, different people. Still concrete, still specific. + +**Wait for response.** + +Continue capturing until operator has 2-5 scenes. After each: + +> **[1] Add another scene** (max 5) +> **[2] That's enough — move on** + +**Wait for response.** + + + +Read back all scenes, then: + +> Your scenes: +> 1. {scene 1} +> 2. {scene 2} +> 3. {scene 3} +> ... +> +> What's the thread? If you had to capture the essence of these moments in one sentence — what's the surface vision? + +**Wait for response.** + +If the summary is too abstract, push: "Make it more concrete. What would a camera capture?" + + + +> ## Your Surface Vision +> +> **Scenes:** +> 1. {scene 1} +> 2. {scene 2} +> 3. {scene 3} +> +> **Summary:** *"{summary}"* +> +> **[1] Lock it in** +> **[2] Add or replace a scene** +> **[3] Reword the summary** + +**Wait for response.** + + + +Surface Vision is locked. + +Return to parent workflow with: +- `scenes`: array of scene strings +- `summary`: one-sentence synthesis +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Surface Vision data: 2-5 scenes + summary sentence, ready for operator.json. + + + +- [ ] At least 2 scenes captured (max 5) +- [ ] Each scene is concrete and sensory, not abstract +- [ ] Summary captures the thread across scenes +- [ ] Operator approved the final vision + diff --git a/commands/orientation/tasks/task-seeding.md b/commands/orientation/tasks/task-seeding.md new file mode 100644 index 0000000..b468ff6 --- /dev/null +++ b/commands/orientation/tasks/task-seeding.md @@ -0,0 +1,93 @@ + +Seed initial tasks under projects after initiatives and project mapping are complete. Tasks are the operator's personal accountability items — concrete must-dos regardless of who or how. + + + +As an operator with aligned initiatives and projects, I want to capture the immediate must-do items under each project, so that I have a clear picture of what needs to happen next. + + + +- Phase 8 of new orientation (after Project Mapping) +- Operator wants to refresh their task list +- Composed by new-orientation.md or reorientation.md + + + + + +> Tasks are YOUR accountability items — things that must get done. Not Claude Code todos. Not aspirational ideas. Concrete next actions. +> +> We'll go initiative by initiative, project by project. For each project, I'll show you the current status and ask: "What must get done next?" +> +> You can skip any project that doesn't need tasks right now. +> +> **[1] Let's go — start with the highest priority initiative** +> **[2] Skip task seeding for now** + +If [2]: Exit gracefully. Tasks can be added anytime via Apex MCP. + +**Wait for response.** + + + +For each initiative (highest priority first): + +> ## {INI-ID}: {initiative title} + +For each project under the initiative: + +> **{PRJ-ID}: {project title}** ({status}) +> Next: {current next action from project data} +> {Blocked: {blocker} if applicable} +> +> Any must-do tasks to log under this project? +> Type them out, or say "skip" to move on. + +**Wait for response.** + +For each task the operator names: +- Create via base_add_project(type="task", parent_id="{PRJ-ID}", title="{task}") +- Confirm: "Logged: {task} under {project}" + +Move to next project after each response. + + + +After iterating all initiatives and projects: + +> ## Tasks Seeded +> +> {For each initiative → project → tasks created} +> +> **Total:** {N} tasks across {N} projects +> +> **[1] Done — lock it in** +> **[2] Add more to a specific project** + +**Wait for response.** + + + +Task seeding is locked. + +Return to parent workflow with: +- `tasks_created`: total count +- `projects_with_tasks`: count of projects that received tasks +- `completed_at`: current ISO date + +This phase is complete. Parent workflow resumes. + + + + + +Tasks created in Apex via MCP under their parent projects. Operator's immediate accountability items are captured. + + + +- [ ] Each initiative's projects presented for task seeding +- [ ] Tasks created via base_add_project(type="task") with correct parent_id +- [ ] Operator could skip projects freely +- [ ] Summary displayed with total counts +- [ ] NEVER treated tasks as Claude Code internal todos + diff --git a/commands/orientation/templates/operator-json.md b/commands/orientation/templates/operator-json.md new file mode 100644 index 0000000..195a721 --- /dev/null +++ b/commands/orientation/templates/operator-json.md @@ -0,0 +1,88 @@ +# Operator Profile Template + +Output file: `.base/operator.json` + +```template +{ + "version": 1, + "last_updated": "{iso-date}", + "hook_active": true, + "operator": { + "entity_id": "{apex-entity-id}", + "name": "{operator-name}" + }, + "deep_why": { + "layers": [ + { "level": 1, "question": "Why do you do what you do?", "answer": "[Layer 1 answer]" }, + { "level": 2, "question": "But why does that matter?", "answer": "[Layer 2 answer]" }, + { "level": 3, "question": "But why?", "answer": "[Layer 3 answer]" }, + { "level": 4, "question": "But why?", "answer": "[Layer 4 answer]" }, + { "level": 5, "question": "But why?", "answer": "[Layer 5 answer — the root]" } + ], + "statement": "[Synthesized deep why — one sentence distilled from the 5 layers]", + "completed_at": "{iso-date}" + }, + "north_star": { + "metric": "[The key metric or outcome everything aligns toward]", + "timeframe": "[Target timeframe for this north star]", + "rationale": "[Why this metric above all others]", + "completed_at": "{iso-date}" + }, + "key_values": { + "values": [ + { "rank": 1, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, + { "rank": 2, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, + { "rank": 3, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, + { "rank": 4, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, + { "rank": 5, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" } + ], + "completed_at": "{iso-date}" + }, + "elevator_pitch": { + "pitch": "[4-floor elevator pitch — who you are, what you do, why it matters, what's next]", + "floors": { + "floor_1": "[Who you are]", + "floor_2": "[What you do]", + "floor_3": "[Why it matters]", + "floor_4": "[What's next / the ask / the vision]" + }, + "completed_at": "{iso-date}" + }, + "surface_vision": { + "scenes": [ + "[Concrete future moment — specific, sensory, tangible]", + "[Another concrete future moment]", + "[Another concrete future moment]" + ], + "summary": "[One sentence that captures the overall surface vision]", + "completed_at": "{iso-date}" + }, + "extensions": {} +} +``` + +## Field Documentation + +| Field | Type | Description | +|-------|------|-------------| +| `version` | integer | Schema version for future migrations | +| `last_updated` | ISO date | When any section was last modified | +| `hook_active` | boolean | Controls whether operator hook injects context per prompt | +| `operator.entity_id` | string | Links to Apex entity (e.g., ENT-001) | +| `deep_why.layers` | array | The 5-layer "but why?" excavation | +| `deep_why.statement` | string | Final synthesized deep why | +| `north_star.metric` | string | The one metric/outcome that matters most | +| `north_star.timeframe` | string | When this should be achieved | +| `key_values.values` | array | Ranked top 5, each with practical meaning | +| `elevator_pitch.floors` | object | 4-part structured pitch | +| `surface_vision.scenes` | array | 2-5 concrete future moments | +| `extensions` | object | Open field for future operator metadata | + +## Section Specifications + +- **deep_why**: All 5 layers must be filled. The statement is a synthesis, not a copy of layer 5. +- **north_star**: Must be measurable or at minimum clearly evaluable. Timeframe is required. +- **key_values**: Exactly 5, ranked. Meaning field captures how the value shows up in daily decisions. +- **elevator_pitch**: Each floor is one sentence max. The full pitch should be speakable in 30 seconds. +- **surface_vision**: Minimum 2 scenes, maximum 5. Must be concrete and sensory, not abstract aspirations. +- **extensions**: Reserved for future operator metadata (e.g., strengths profile, archetype data). diff --git a/commands/pulse.md b/commands/pulse.md new file mode 100644 index 0000000..0a0a8f6 --- /dev/null +++ b/commands/pulse.md @@ -0,0 +1,33 @@ +--- +name: pulse +description: Daily workspace health briefing +allowed-tools: [Read, Glob, Grep, Bash] +--- + + +Workspace health briefing — drift score, stale areas, overdue grooming, quick status. + +**When to use:** Session start, "what's the state of my workspace", daily check-in. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/pulse.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/context/base-principles.md + + + +$ARGUMENTS + +@.base/workspace.json +@.base/data/state.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/pulse.md + + + +- [ ] Drift score calculated and displayed +- [ ] Stale areas identified +- [ ] Groom cadence checked + diff --git a/commands/scaffold.md b/commands/scaffold.md new file mode 100644 index 0000000..2e4f6ec --- /dev/null +++ b/commands/scaffold.md @@ -0,0 +1,33 @@ +--- +name: scaffold +description: Set up BASE in a new workspace +argument-hint: "[--full]" +allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] +--- + + +Guided workspace setup — scan, configure, install BASE infrastructure. Optional --full mode adds operational templates. + +**When to use:** First-time BASE installation, "set up base", "scaffold my workspace". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/scaffold.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/workspace-json.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/workspace-json.md + + + +$ARGUMENTS + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/scaffold.md + + + +- [ ] .base/ directory structure created +- [ ] workspace.json generated from scan +- [ ] state.json initialized +- [ ] Hooks and MCP servers installed (if --full) + diff --git a/commands/status.md b/commands/status.md new file mode 100644 index 0000000..5d8149e --- /dev/null +++ b/commands/status.md @@ -0,0 +1,28 @@ +--- +name: status +description: Quick workspace health check +allowed-tools: [Read, Glob, Bash] +--- + + +One-liner workspace health status — drift score and area summary. + +**When to use:** Quick check, "workspace status", "how's my workspace". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/status.md + + + +@.base/workspace.json +@.base/data/state.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/status.md + + + +- [ ] Health status displayed + diff --git a/commands/surface-convert.md b/commands/surface-convert.md new file mode 100644 index 0000000..4adf607 --- /dev/null +++ b/commands/surface-convert.md @@ -0,0 +1,35 @@ +--- +name: surface-convert +description: Convert a markdown file into a data surface +argument-hint: "" +allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] +--- + + +Convert an existing @-mentioned markdown file into a structured data surface. Analyzes structure, proposes schema, migrates content. + +**When to use:** User has a markdown file they want to convert to a structured surface. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-convert.md +@.base/hooks/_template.py + + + +$ARGUMENTS + +@.base/workspace.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-convert.md + + + +- [ ] .base/data/{name}.json created with migrated items +- [ ] .base/hooks/{name}-hook.py created +- [ ] workspace.json updated with surface registration +- [ ] settings.json updated with hook entry +- [ ] Original markdown file preserved + diff --git a/commands/surface-create.md b/commands/surface-create.md new file mode 100644 index 0000000..77d6167 --- /dev/null +++ b/commands/surface-create.md @@ -0,0 +1,34 @@ +--- +name: surface-create +description: Create a new data surface (guided) +argument-hint: "[surface-name]" +allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] +--- + + +Create a new data surface through guided conversation. Generates JSON data file, injection hook, workspace.json registration, and settings.json hook entry. + +**When to use:** User wants to track something new as a structured data surface. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-create.md +@.base/hooks/_template.py + + + +$ARGUMENTS + +@.base/workspace.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-create.md + + + +- [ ] .base/data/{name}.json created +- [ ] .base/hooks/{name}-hook.py created +- [ ] workspace.json updated with surface registration +- [ ] settings.json updated with hook entry + diff --git a/commands/surface-list.md b/commands/surface-list.md new file mode 100644 index 0000000..3b01d3e --- /dev/null +++ b/commands/surface-list.md @@ -0,0 +1,27 @@ +--- +name: surface-list +description: Show all registered data surfaces +allowed-tools: [Read, Bash] +--- + + +Display all registered data surfaces with item counts and hook status. + +**When to use:** User wants to see what surfaces exist. + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-list.md + + + +@.base/workspace.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-list.md + + + +- [ ] All registered surfaces displayed with counts + diff --git a/commands/weekly-domain.md b/commands/weekly-domain.md new file mode 100644 index 0000000..8a362f9 --- /dev/null +++ b/commands/weekly-domain.md @@ -0,0 +1,34 @@ +--- +name: weekly-domain +description: Create a custom domain phase for the weekly ritual +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] +--- + + +Guided creation of a custom domain phase for /base:weekly. Walks the user through defining what to check, what tools to use, what questions to ask, and what output to produce. + +**When to use:** "Add a domain to my weekly", "create a weekly domain phase", "I want to track X in my weekly". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/weekly-domain-create.md + + + +$ARGUMENTS + +@.base/weekly.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/weekly-domain-create.md + + + +- [ ] User described the domain area +- [ ] Data sources identified (with tool discovery if needed) +- [ ] Weekly questions defined +- [ ] Position in weekly flow chosen +- [ ] Output type specified +- [ ] Domain phase config written to weekly.json + diff --git a/commands/weekly.md b/commands/weekly.md new file mode 100644 index 0000000..990f5a8 --- /dev/null +++ b/commands/weekly.md @@ -0,0 +1,39 @@ +--- +name: weekly +description: Weekly review and planning ritual +allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] +--- + + +Guided weekly ritual — close the week, plan the next, run maintenance, lock in priorities with calendar events. + +**When to use:** Weekly planning, "run my weekly", "weekly review", "time for my weekly". + + + +@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/weekly.md +@${CLAUDE_PLUGIN_ROOT}/base-framework/context/base-principles.md + + + +$ARGUMENTS + +@.base/workspace.json +@.base/data/state.json +@.base/weekly.json + + + +Follow task: @${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/weekly.md + + + +- [ ] Week reviewed (daily logs consumed if available) +- [ ] Calendar audited with rules applied +- [ ] Workspace groomed (drift score updated) +- [ ] Priority stack set (outcome-based, aligned to north star) +- [ ] Backlog triaged (overdue items processed) +- [ ] Domain phases executed (if configured) +- [ ] Blockers identified, follow-ups queued +- [ ] Week committed — calendar events created, weekly.json entry logged + diff --git a/hooks/__pycache__/active-hook.cpython-314.pyc b/hooks/__pycache__/active-hook.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..944c4d1827184081f8642fc4c703a0b1030780b7 GIT binary patch literal 10313 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

ZD&0XwkAo)JY*{-r2Oj6pU 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 IQ4aP013W+6LjV8( literal 0 HcmV?d00001 diff --git a/hooks/__pycache__/backlog-hook.cpython-314.pyc b/hooks/__pycache__/backlog-hook.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02e7fe1f82b71b0edc979bb32017b4714679a7d7 GIT binary patch literal 5636 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 E0(~T`C;$Ke literal 0 HcmV?d00001 diff --git a/hooks/__pycache__/base-pulse-check.cpython-314.pyc b/hooks/__pycache__/base-pulse-check.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a6b8f48b3c1d7c60d4e211fd12e1737dd9e7140 GIT binary patch literal 10538 zcmbtaeQ+Dcb>GAHC-F^?#0QTbA_;zg6s3qFMM(s`MHJ&+mu5&!c*TeuICAkE~dE#7GcIAwS~K zRpckm5F8=AC7cA_k~7i~DJLC~ak6^cFFhk4QE-ZSbVbpCt|$)@1uj05$hQ1GSgiWY zQNt$;I+U%aB(YaM&Fapo&#zr0ghmcu7bTyAh%V&mt57>w`5p8%nFK2McQw{yc_|1`JPgV(9!ZIg&_RCvWr)kkcWS>Y|e| zD8sW<_#m2ot@=b&?^53bCE!43kLAk>N!A zT6D%!!Ernpo+a7n937(=lA8_1$s7C0o3l}9r9%u0e9m7Ox-jgWi;x`k0!MZLk2yM7 znt;5<0N-oT7)8!;(K(9sYA!I*nHh><2gr*o#rU9~=FTUl=b{|hCN2yF!R$Pm$s`Bd z2FSs(vW}5AcPj=jGs%((rnnAJ+< zz2y0uA({kYmg2Z*e1GUdM^88bO-Sf~IUb3woMK zJWqwWfP*Ub^M+>2-h{3)*@S{0BP(dkDB(dvMDPYx<^4!`>F zO*l+IgHAN+Kz*K`)~_q+MSp1n#6uAamNB64!P<5|0iC$2dNf`W(EIhm!3v*L7@_>U z5l@hzO5{mb1*$|%%@p+e3xsW?B*qi8Rpwy0RDbwyU=3j7Ad%`S(OF?Nv}zP~c=UG& zsgWC2D9%-&o^pxP1Qen~<6!XtiJxeJu7L81^rgU~NH4+km%i?o3ggh8_GIWTTci^0o$Idu#|PQXMf2{=;}aqX?H*}Cz|>^533eu|NnZ})v5}MGwrfC z)9A0j;$Y`Se_gaX6WEgi+r^Y9F!EdNU}Zj!sGss%)Rgh@lg{q91yDbn4iN=jZ#N1c zzfG_=JFa!smAUwe*5K4bS0(=1zT!D8?oU0mHz5*DRg08U5HJ^>0}=&_DDnDnIza1OXjn3K zBZ`hlT_{R?#u!Cm45lCbAA~AKlr7pV&Qg9)kz!q#Szh8ojBS5$9_1Askit63G7+Pn z?2xH6D^Qi@yZ~Ae=kz!h-t<$Y*s%~gLZB3hhC^I}VGBWCEP8_?anvaUAW0erH8?^o z2B2Pz=E5ulAXJ=9G89{6!iEzJCB!AMo1uBkiDG9T3voLkXyGug7^M*cN^X`SVh-?g@Fb6^gLo-w`0<-bP=}`E3EHP7V zv4|}+a1S!njVN`KHzgPvLN_WBEXJoSZ%xKI1_nbLM9y3gN4UUG5HZNYnq#pj2Q6|a z=U86G&C?XGEXGm1rXXx27~*&ZR#B4W5w9%H&8rLZ2B+tFO$9F*4dEd~Uvwx5(HAa- z=!@3~fi)MJquCvi80fK6tPc}r9;z$`2Xi+&5MP8%jN4)0Rvhwb;z{mk++>J444cl<8#T_Wr7{x^$V z^6Tzy;s=^;f|m>S!^;J`gvFF>oY!M}3(h4X(Q8qN;R#-WQ;axw3@Z&bI1x#}4uT+H z6TCVa7mY^?RoM3#Fl^RS$3WnLcsWpq=6QWF#6KTAeP(Q!S5PlRSq_W^=WDzi)D~f} zErcRLoDA{O8H&UB1>*JC38HKQG&ct(1o;CPvnCLVC8=SCNie+n0@z!jstD0E6_4=J zxzG!|;q3XziBnWK5h?a6C(jFHOcmx^b!pN+dfqp5ayaOpoV>tG+2kCrxPkeyyfmJ; z2@5ODsu423g6Nu?3>bPrOnD>;mguPgC1U4ijsj<<8M9Z3c|>70(H#p805fRI{>kz> z(G$U2p{vp#W!R~BWzmUvo#;*Y#Nnkt&tMdWvEezjMPI^w3_jz*rSLg|ofK3Z4?+GV zg*vZ-B8WYV2R}>3cnQZcO+e4$&q0frVefh~7_#9_k69#h9@_Dm*%x8MQ)pX1DjGqz%1ZOmG`7KcApp!x}7 zaWY@$TpZ0iT#IA*I>+MZZ)#jivb@E9_wb#=%crxJrj_}1OV9gO=ZAIfdu=bZtqf-C zdX|(AEp=NGnT_0R?as9xeb9RJ{!F&@bgp$Y(>j`M9beYv>l-)f_vhSw8F$}HQdp3~ zxpeZwTGzdrmugnpR!?MWyO$JsOYOR)GjFZWSz9vJmKAox+L5<8mgm=PZFwu1v$kfe zt*g>?Yv+~%xf=QKoLtuC9qx5U zXWrSmx_`sj`SR#TQsnIV=m4s9|9%h+5o5&bC)c#=orCFf{>8Itds{~5d3a*t_48}a z^{!*-3m0*tC!=f6_Z&&vJ2SejEgdr3@7CU_T^?LDXHA`{vw4#l3R`{7wl8Db_n)@b zTk1vGVtCWiuyXR%XTJZ;>glx;+2+0tOW*z04a?9QqZ^jVMO9vFxqbZB@toGVu61rn zRC-%+r&ii>jR!N02eXZbvi3gEzODA|vv;0dj)9hJ-8tKVjP1bMSk^YQBzb7DZ8mk} zn))9!_1|yIHXY41oyatu$Tpo?8q3!>Hfq{)_MVKrXRUYL{-u0P<4Wl74KHgz1vQ60 zk|S&NM|xDABCDH74TH{Ah9zoU z>B!eQ7UhdGx2p2)rqu9a>t9SP^<`v^d{dWDAI!+eEtyQN-)!)_>ih@iKe}@b{h5aT zY{L;4mdL2wn<{fo<;tjB-;3OP{^jRaZEIg%clWQW2EZ_MrqtLc+w%l+wZ5)SJ9@Vf zVI%YI`oEKINul=Xc5`Kq>w_96Zg+jUU4=}xoUu7$Y<^`XZScZ4wTagmp;vACbelxB z+MKm5V{LoA^@js#%Q5KJ+deQ_p<+t4|O0pUdsIbXlQ%yM9OqV9mdG4$0v4OJc}0X+rN< zE#n>XcUq-D@J>5{>m5SfYnqVB-#IRW_IITO)Zdli_IH(zi5BU*`v_cm$jK`CdwK$w zCha6CeXpK?G7N&19{fqdABsmkP)k*2sJM04BSy)kFSQ-Vjpc8-561z!o(70#%MtKX z;3TE-PnB_@Dai5*&$Fd@Xar~oF=Z8de5Ht6Y(c1;573eT^3|4m#G0QdXKex=vlop- zTpKM5$V9zF{uJv4)>@XJB8w6EL!MBA+-doxeM0sHpD@VxrF+UF1&F>FLD7m!`*t8E zdw~43GC=GJ^veTE9pGI71(eEOxwrzHpX@oWszMK{fND<~HSDq|oB9cSG!=XROZu!U z1tYqsy{Jn{y(lR;4PQQt7cl=C(Cs<5R$PI<8RI_1_|XONqjo|v3ZWT}9cewS1@uT? z+M_l>(|cjfX;1x_{T6X=#MJ|C`6qb&e(P1G-*&aiUwzfxy{EiS;A!_)i#)Ho#TspR z0(FhJ4%!$fUoDM1rI(a0lK@+i1Wcc6_vU~(U=X|s{2sh%?>(*!m|>6WDs?<=dIGIe zl)oNLwTSlMdx{e>@|VRm!s%Lgo`4RQxjqffWC>XI+-X-C9R?4wCMAOZfF}r;*k0#X z*>~L7a*Mu$(O@h14tSp6J2n_^6KyBbRM4t_-x0|X$qsy`eyW811Z<+tK+9yWv=YBe zN|OSz7$8bK1&^63UC8+xrb;Ny)e<_kr#BK6^E6iO<`?-c`I`h}ro2lyN!j(3@g)H> z0Ucqq=8Mb%D2;UNvWRs6szZuXRN^6k{JbU7BqjAIDW!F^Hi~x93$|U4F6?dH4pA%h z2xcY5rgm_Y&{l!p7g?{YuwEA$+Z1_uT7HWcz{`a@J-!OkW-chygOMb~YlJ+u(5Fns zuR~TBUuZZ1HeH!ze1kJsaoNV5?Umk0pFd92Vd6pIx`kl`f5K!(oi@J&^4c77%b7y_?`EVPhlp5_%dDe8J` zenCAQ?J87xWjF-+J-`?$L1RoZzEFK~$UjCF8IhuP+7>(!xFZG90wx8N zJJf<_?!~4si>Zf$aH2G|sEB9)cLl;H5DNuo9ZVP?Iz~%Xq4p6F|-D62i?fWb_Oq z@({owCV<;jf;(VGP(cAOAix4}#Nq)3V;7J?!AyCr00spGEQo>V5it{(Qicte*9wad z7L52g+-@lDaj{wzQZ zGVEH+JW$=jI=Hpn|OT4zq{exQYPOnY1PqH4+X9qpD(Dz|PLt#>uw)qD?f z!0wErJL}k=HTEvbw>3!bUZ%3T=G5?q7F%j;Q&UxfC?Ib%-#&Be%+kyc=oOK;v z(&WjeA5`V)4`%8QF6r_{XU^D^F*fC^`|j)3zPg2o(?oN=*}F0b)t*ztmQ-RJCbp$A zlX=Vhm>7_(d+vCapIJGVHMgcl^E%T`G26TUx#P8t*SpruGp}{MbawgF{ewR~{G-Ef zjAW0Dr_Wu>9JzM);L^d@x>BPV^UQxK&3TnIr*dRej(iol;#kqAyN=$E{5blf=-b-t zu?y*=7t^}H@1zp72J&T%ZJAhC*XC_?cQ4<$y!@5b%UN6BqNajH`|7}IXZq0Dv~F}; z7^Feg+PnRC`j@Y+`m>gv)L33=$tmp_rF~huaxtgu$SOPXj;57!tJ*dH{n|H9r0br^ z)s1HAU}ZHqoin3zE?-_%XLY@6Cmu;94$Yzg1Of2G(v$)8=4{r|y{LMqwP3^{Ol*?P zIkNWw*}HZ)OAapS^X`U~@eTL>oU1S6>Vu82xK<47mhKYzlCSGot6S5h2dQ+;HIUd= z{Rsdf`r4eXKBKEolh3W|rT|0GfqgkLT1QUndY}ayN&)DGle~BK<+HEM=UN9ctpnNC z!L0M>k~-hg{p#HJ=hn{LXVzOzWb4A2mJ=(cH$wkL{e*gZO}#$eh;6190-h2*MljeLD%@!U5@KiPgxg7l`Gj?C!DS6b8B zb_@ytx1xvK)&gE2w|=_qMaH@v5W^oIRUykP@yYffWOZY3AxxAkH{Be{8K*JQ5GMZk zu^BlcK&PlcB+?d)bLegF7x9Qcuyf$8|I#*o9R0LysAi%My``?3R4d+UQ$YFlksZITpFB{-O~5G1TK5! zK>1#u!)KGGtpt?3Dj39PMKI`b^U7c_k_ZQb%ry|Ba1ul6h6-S`5`&(x=rjYd1e}jS z;VvT>bGGAKF}ZgLi^CTSvy$k($Ws>Xt8U=$dod&WyG* zr}aM2dh;s7?W$W`A|`!dxxPXkOSt7HBORP%pMU&d_! literal 0 HcmV?d00001 diff --git a/hooks/__pycache__/operator.cpython-314.pyc b/hooks/__pycache__/operator.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f10311379b81d290558987091d2b2e7f29c4df6a GIT binary patch literal 3189 zcmbVOT}&I<6}~eb+wC6~{ z_o42~3bqdr^&vb|6%SR#J~c0uc&t=u)khp>cbvOhZL2=?mEELjAF8Tn3z~^NVw119AA6;-G^f#+xH|j`u{T~9MG7^x1eu4yIk}wG1*5x>Kf=+Ex{e~Mtbe2lkhZ-9?FKhZj&~cI%f?o#U$EK>m|BQj|mRjtKm!e zt{f?3qX>1QlrPbHdWDVTSQk8jt`&jLw)MXvFap!_Dfiprc-xo}NCgCHuwh3M7thD^ z5Oe?&mw4y2Zo&rMFeNxsfkD^FfE@>%%y+yCu0||Ii{NfUMkv)P&^PWQl!Z(Hzo9;4 z?@}+E_Qm=aW=g+j?~Kfi^An2lc$imINlP1w7FE~SIi;v&q+ve#AgxQW6|Ww*_kkbsYKdX#!aIij?9mZ ziwR+NYJ4;)j!gQt#S{Cm&w?`w#`b%qW9IW__fHrz?4ea_jYdbR}CZOlKuY6<3$nFaz3#WYsXtK9<(R zm2|Es>DYC|opp|VdQn?SXC!e&)@7xD-A7dr`=p$-VwF}=m5t0YcGg*m9OO$R-;arK zsqLhgvNIMweh%;r^CgZQ0}%YeGStZu!``bqJ{0RtK&07(Wfde3d+WGX-wv~w85>EC zi1En}$FWO#BI|~ZDXUOnXHH3Hbyg#-7uA;c9TKJXH&{Jfok|+ub`_H4rfa(Mlk?`_bN= zYT$?E$r?C%vhie_-MLfsUorgy75~8Q-Mx;gf3!SNi$s6d|7rj8TYqS(M#jv@L?tru zr}q81`InJFw<25J+rkdB{n2h;#Xq=LDo-3xM3cAX4XsZdAfk<{an0)ykU~t2XYwe0(cYJn@0E%f*w9Zu&!)@3tidiKiiW(G7gxxA! z_Br{1W?fL0-jbY?L=n3+Nmp_!5_YL+&H6d%iP_n^)++#$3jChpgJlu8LtoRuB6RA4 zR9KNUrJz}l&qEzGi=B|5vWk5Rv%=lE#K`ElD9p|#u}jv)tPF)hH?oqZVdttQ8w$;qTX`j0%t^Pko6xmh(fSuaU@HjX8_G#guYHK{{28_X zmGf`WzqoC3eHE^+%3U~qh1W#(HKPc$9G a2}F6;OD`QAuPFNC_%{wjb!x*9+J6B9*{Rw9 literal 0 HcmV?d00001 diff --git a/hooks/__pycache__/psmm-injector.cpython-314.pyc b/hooks/__pycache__/psmm-injector.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c23c4737edfbbc8bd834746c43345ab64121cf23 GIT binary patch literal 3662 zcmb7GT~Hg>6~3!oNvmD`fdv>a;es5_NhEY*l||^UWMVaXckt*t3ZXm_kb$ltff}IO)P$9E@DfBovt*l@p-e5jwDAvB&x@Fo*b>ykP&2nWKJYcrRTc-U?1j_P=@iB6_F1 zQLyakX2s32es+JwdhSIu3v3HA{8J5r>%74_5xYT;`w+A#PYv zZVq5oPo;u#`i3-PC|XF(a?y;YD!Md)qmmXJmvmiL()f~Oh(J<`mc@U`{~YfKU`d^m zQj#VnvA)ZwOzV<1FB%}EbJ%Kf+EB1GFM+hGDJj*!x`K^4DT{SO%;Gu40F5}Qq-QNf zI)3%YCA=V~6Ustwh^wiZfSs_2bvZqolq%MTZs=g)bs44$vN30A2;P*kQUXh9Lz5*v zRMQX6B8rn>U518B5b?+5$w_= zJUVt6iy1=+&Lkx<4Hl4i8FQl1qZ600qC&tR)-b)HOxMhsmqk2yR=9}gva_<3mT(xq zadtd{6PkS8zzNBK6N0EhEG}#E?5w2e0~ms#MJoXv&rGLegB!~jYQ`AAgBeW&&n?Sf zuju%tOI9$jsAD~=8&V3-C}~5wWgNna1~E+%8TmSv;E1vzZnT#&nW$*YnT>-N&rY0+ z#G}I4n~}lG@pB_W&K{Z;b;)EBq9Nv7B#$a5VN#))`UH2wgdonN>kL7gb~IW28ih1` zx^2nDix!w`<7@Yw`H=5TG4%$ zM6I{>uw-UiDl#=&12tZ2)CIU|+0^;}cwVyrv_IV}3oMPQ?TT$rFKuB|^^%U!d-24m zsum{3SUx2xxD#XclBnUV#r@(Dh%o|vsIBIgY7ZgR5wrWz`Ei6)XUw_h&WvEM`olhl z$5D5(uIKPL>v*`BW6x^B#yB?;Trn0X7jwDM(Q3u(L>DFya-o+{%wfeK0V@a4b0WqI z?yBAVB;?pmi`lEOcTaX$tAZy+3EukXfCavo8>m0V18tbBR*q|&r6Jm7$=CW=)nbrp zJnFJ|JL_-O|IAil-$8`y?<%>+Pqj&}wQ$VlKnW*_1*tPC)qx_^#b3$jB;A2z>IL;w zZsUttTiLH*?bjPHxm`?-%Lx;itcB2~bGIItjFD9Vya25uFa`)TU51>JpdO{v938|z z%GsuH&NhHe=Zq$ahLkX!ne@#x)QOz$6hS)+Tu*lojQ$XPC1FB$ zpW$*WcPdOI&y4mqm<%afrah^M3EiXtj!nC6B;>Toz;+3fhRS66-W(qrJtxgT1&U~z zqM6RI@tuK-;CcM~$i;~1kZ#Gkp_>f!;02M6R~&MPsw$;H6~MY_S2d`@COtE!!PbhL z?)8|QoQ7%BhN^%Qcg=?APHX9{4x;}G#>HW-G#D|MbM+#iIsdMGp?Q3fn*Sm{NuNAl_EOIx0 za<$YE+~^3FI>HY+!dooA#D2&wH~f^}W@vk0%eQYS_hIgn-cnm|qb*o$3m1Kd3(Pi) z{DGxgi?>#}wKs~sp8Rmx+q{%qOs)C@1_I%p^i~cpa)OPqm+u?e>4&-ZI zmN&gPKU8*m^O0{F_808srnZ%mLSU;eu#{cQt~ReV7JV-lm~xdiYxi-AzVRc1X)?nU>qzWVxV|C(@zTl?|)(e=c7?0(a!Kd`4Cx=>^56E|W#<;K>P z{@eYl@}1UV5_uS|E9wW-WzueUF5w}gl=wFZh&wdY71GB^vuE0O_m*F_yJIf5Upx^x9)uEH< ztCo(T6ZBW#rwBcHWcVIz)2yAJMKuo6!?~9z8o4i3?+6mkAck%bJqb zNU=5Dad?`NXK*|rYZ|Fi8iDLe9uoa&F$Ma6i5|fs(t3fI?5UKJ$RwpR+UqcdG)4OF zfPl9Y^^kE>%%cN{;=V!fWL@qHV7U!E;Dw61jC?kqL* zY&7)by?^#KEaet+OYbbcbN{8IMc=W!`%kp*Yky#Qvgq$#du!8w=+6A6|FygPrho9= zk#FI{_{Xl2ziY$aRrDWRo89yW*BzVw@7-TrjaOmH|{`$=T literal 0 HcmV?d00001 diff --git a/hooks/__pycache__/satellite-detection.cpython-314.pyc b/hooks/__pycache__/satellite-detection.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c3c60cb10006f8fc32930b15c2a258f8e56dde4 GIT binary patch literal 15549 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 literal 0 HcmV?d00001 diff --git a/hooks/_template.py b/hooks/_template.py new file mode 100644 index 0000000..802e863 --- /dev/null +++ b/hooks/_template.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +BASE Hook Template — Canonical reference for data surface injection hooks. + +THIS IS A TEMPLATE, NOT A RUNNABLE HOOK. +Copy this file, rename it to {surface}-hook.py, and customize the marked sections. + +=== CONTRACT === +Every data surface hook MUST follow this contract: + 1. Reads ONE JSON file from .base/data/{SURFACE_NAME}.json + 2. Outputs a compact XML-tagged block to stdout + 3. Wraps output in <{SURFACE_NAME}-awareness> tags + 4. Includes a BEHAVIOR directive block + 5. Exits cleanly (exit 0) — never crashes, never blocks + +=== DO === + - Read the JSON file using absolute paths (Path(__file__).resolve()) + - Format a compact summary: IDs, one-line descriptions, grouped by priority/status + - Include item count summaries + - Include the behavioral directive (passive by default) + - Handle missing/empty/malformed files gracefully (output nothing, exit 0) + - Keep output compact — hooks fire every prompt, token cost matters + +=== DO NOT === + - Never write to any file + - Never make network calls + - Never import heavy dependencies (sys, json, pathlib ONLY) + - Never read multiple data files (one hook = one surface) + - Never include full item details in injection (that's what MCP tools are for) + - Never include dynamic logic that changes based on time of day, session count, etc. + +=== TRIGGERS === +Register in .claude/settings.json under UserPromptSubmit. +Use `which python3` to detect the absolute python path for your system. + { + "type": "command", + "command": "{absolute_python3_path} /absolute/path/to/.base/hooks/{surface}-hook.py" + } +""" + +import sys +import json +from pathlib import Path + +# ============================================================ +# CONFIGURATION — CUSTOMIZE THIS +# ============================================================ + +SURFACE_NAME = "example" # CHANGE THIS: your surface name (e.g., "active", "backlog") + +# ============================================================ +# PATH RESOLUTION — DO NOT CHANGE +# ============================================================ + +HOOK_DIR = Path(__file__).resolve().parent +WORKSPACE_ROOT = HOOK_DIR.parent # .base/hooks/ → .base/ → workspace root is parent of .base/ +# Fix: .base/hooks/_template.py → .base/ is parent, workspace root is parent of .base/ +WORKSPACE_ROOT = HOOK_DIR.parent.parent +DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / f"{SURFACE_NAME}.json" + +# ============================================================ +# BEHAVIORAL DIRECTIVE — CUSTOMIZE IF NEEDED +# ============================================================ + +BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. +Do NOT proactively mention these items unless: + - User explicitly asks (e.g., "what should I work on?", "what's next?") + - A deadline is within 24 hours AND user hasn't acknowledged it this session +For details on any item, use base_get_item("{SURFACE_NAME}", id).""" + + +def main(): + # --- Read hook input from stdin (Claude Code provides session context) --- + try: + input_data = json.loads(sys.stdin.read()) + session_id = input_data.get("session_id", "") + except (json.JSONDecodeError, OSError): + session_id = "" + + # --- Guard: file must exist --- + if not DATA_FILE.exists(): + sys.exit(0) + + # --- Read and parse JSON --- + try: + data = json.loads(DATA_FILE.read_text()) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + # ============================================================ + # ITEM EXTRACTION — CUSTOMIZE THIS + # ============================================================ + # Default expects: { "items": [ { "id": "...", "title": "...", ... }, ... ] } + # Adjust the key and field names to match your surface's schema. + + items = data.get("items", []) + + if not items: + sys.exit(0) + + # ============================================================ + # SUMMARY FORMATTING — CUSTOMIZE THIS + # ============================================================ + # Build compact summary lines. Keep it SHORT — one line per item max. + # Group by status/priority if your schema supports it. + # Example format: "- [ID] Title (status)" + + lines = [] + for item in items: + item_id = item.get("id", "?") + title = item.get("title", "untitled") + status = item.get("status", "") + status_suffix = f" ({status})" if status else "" + lines.append(f"- [{item_id}] {title}{status_suffix}") + + # --- Output --- + if lines: + count = len(items) + summary = "\n".join(lines) + print(f"""<{SURFACE_NAME}-awareness items="{count}"> +{summary} + +{BEHAVIOR_DIRECTIVE} +""") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/active-hook.py b/hooks/active-hook.py new file mode 100644 index 0000000..ba3f981 --- /dev/null +++ b/hooks/active-hook.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +BASE Hook v2: active-hook-v2.py +Source: .base/data/projects.json (APEX unified project management) +Output: compact summary grouped by priority +Filters: items with status NOT in [backlog, archived] + +Drop-in replacement for active-hook.py. Swap in settings.json when ready. +Legacy active-hook.py reads from .base/data/active.json (unchanged). +""" + +import sys +import json +from pathlib import Path +from datetime import date, datetime + +SURFACE_NAME = "active" + +HOOK_DIR = Path(__file__).resolve().parent +import os as _bh_os +import sys as _bh_sys +_bh_pd = _bh_os.environ.get("CLAUDE_PROJECT_DIR") +if _bh_pd and _bh_pd.strip(): + WORKSPACE_ROOT = Path(_bh_pd).resolve() +else: + WORKSPACE_ROOT = HOOK_DIR.parent.parent + if not (WORKSPACE_ROOT / ".base").is_dir(): + _bh_sys.stderr.write("[base-hook] CLAUDE_PROJECT_DIR unset and no .base/ at %s; hook is a no-op. Set CLAUDE_PROJECT_DIR.\n" % WORKSPACE_ROOT) +DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / "projects.json" + +BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. +Do NOT proactively mention these items unless: + - User explicitly asks (e.g., "what should I work on?", "what's next?") + - A deadline is within 24 hours AND user hasn't acknowledged it this session +For details on any item, use base_get_project(id).""" + +PRIORITY_ORDER = ["urgent", "high", "medium", "low", "ongoing", "deferred"] + +# Staleness thresholds (days since last update) +STALE_THRESHOLDS = { + "urgent": 3, + "high": 5, + "medium": 7, + "low": 14, + "ongoing": 14, + "deferred": 30, +} + +# Statuses that are NOT active (excluded from active view) +EXCLUDED_STATUSES = {"backlog", "archived", "completed"} + +# Types to exclude from active awareness (checked via MCP during grooms) +EXCLUDED_TYPES = {"initiative"} + + +def days_since_update(item): + """Calculate days since last update. Uses updated_at (ISO datetime).""" + ts = item.get("updated_at") or item.get("created_at") + if not ts: + return None + try: + d = date.fromisoformat(ts[:10]) + return (date.today() - d).days + except (ValueError, TypeError): + return None + + +def main(): + try: + input_data = json.loads(sys.stdin.read()) + except (json.JSONDecodeError, OSError): + pass + + if not DATA_FILE.exists(): + sys.exit(0) + + try: + data = json.loads(DATA_FILE.read_text()) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + items = data.get("items", []) + if not items: + sys.exit(0) + + # Filter: only active items (not backlog, archived, completed), exclude initiatives + active_items = [i for i in items if i.get("status") not in EXCLUDED_STATUSES and i.get("type") not in EXCLUDED_TYPES] + if not active_items: + sys.exit(0) + + # Group by priority + groups = {} + for item in active_items: + p = item.get("priority", "medium") + groups.setdefault(p, []).append(item) + + # Workload balance header + blocked_count = sum(1 for i in active_items if i.get("blocked_by")) + ongoing_count = sum(1 for i in active_items if i.get("priority") == "ongoing") + deferred_count = sum(1 for i in active_items if i.get("status") == "deferred") + working_count = len(active_items) - ongoing_count - deferred_count + lines = [f"Load: {working_count} active | {blocked_count} blocked | {ongoing_count} ongoing | {deferred_count} deferred"] + + for priority in PRIORITY_ORDER: + group = groups.get(priority, []) + if not group: + continue + lines.append(f"[{priority.upper()}]") + for item in group: + item_id = item.get("id", "?") + title = item.get("title", "untitled") + status = item.get("status", "") + category = item.get("category", "") + cat_tag = f"({category}) " if category else "" + parts = [f"- [{item_id}] {cat_tag}{title}"] + if status: + parts[0] += f" ({status})" + # PAUL signal (phase, loop, plan age, handoff) — only if paul data has real values + paul_info = item.get("paul") + if paul_info and paul_info.get("is_paul_project") and paul_info.get("phase"): + paul_parts = [] + p_phase = paul_info.get("phase", "?") + p_completed = paul_info.get("completed_phases", "?") + p_total = paul_info.get("total_phases", "?") + p_loop = paul_info.get("loop_position", "?") + paul_parts.append(f"Phase {p_completed}/{p_total} ({p_phase})") + paul_parts.append(str(p_loop)) + # Plan age + last_plan = paul_info.get("last_plan_completed_at") or paul_info.get("last_update") + if last_plan: + try: + lp = last_plan.replace("Z", "+00:00") + if "T" in lp: + lp_date = datetime.fromisoformat(lp).date() if hasattr(datetime, 'fromisoformat') else date.fromisoformat(lp[:10]) + else: + lp_date = date.fromisoformat(lp) + age = (date.today() - lp_date).days + paul_parts.append(f"plan {age}d ago") + except (ValueError, TypeError): + pass + # Handoff flag + p_handoff = paul_info.get("handoff") + if isinstance(p_handoff, dict) and p_handoff.get("present"): + paul_parts.append("HANDOFF") + elif isinstance(p_handoff, bool) and p_handoff: + paul_parts.append("HANDOFF") + parts.append(f" PAUL: {' | '.join(paul_parts)}") + + # Revenue signal + rev = item.get("revenue") + if rev and rev.get("amount"): + rev_type = rev.get("type", "") + parts.append(f" REV: {rev['amount']} ({rev_type})") + + blocked = item.get("blocked_by") + if blocked: + parts.append(f" BLOCKED: {blocked}") + next_action = item.get("next") + if next_action and priority != "ongoing": + parts.append(f" NEXT: {next_action}") + deadline = item.get("due_date") + if deadline: + parts.append(f" DUE: {deadline}") + days = days_since_update(item) + threshold = STALE_THRESHOLDS.get(priority, 7) + if days is not None: + if days >= threshold: + parts.append(f" STALE: {days}d since update (threshold: {threshold}d)") + else: + parts.append(f" updated: {days}d ago") + lines.append("\n".join(parts)) + + if lines: + count = len(active_items) + summary = "\n".join(lines) + print(f"""<{SURFACE_NAME}-awareness items="{count}"> +{summary} + +{BEHAVIOR_DIRECTIVE} +""") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/apex-insights.py b/hooks/apex-insights.py new file mode 100644 index 0000000..ec778d3 --- /dev/null +++ b/hooks/apex-insights.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +APEX Insights — On-demand workspace analytics +Computes velocity, stall detection, blocking analysis, workload, and dependency chains. +Invoked by /apex:insights slash command via !command injection. +""" + +import json +import sys +from datetime import datetime, date +from pathlib import Path +from collections import defaultdict + +WORKSPACE = (lambda v: Path(v).resolve() if v and v.strip() else Path(__file__).resolve().parent.parent.parent)(__import__("os").environ.get("CLAUDE_PROJECT_DIR")) +PROJECTS_FILE = WORKSPACE / ".base" / "data" / "projects.json" +WORKSPACE_JSON = WORKSPACE / ".base" / "workspace.json" + + +def load_json(path): + try: + with open(path) as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + return None + + +def days_ago(iso_str): + if not iso_str: + return None + try: + s = iso_str.replace("Z", "+00:00") + if "T" in s: + d = datetime.fromisoformat(s).date() + else: + d = date.fromisoformat(s[:10]) + return (date.today() - d).days + except (ValueError, TypeError): + return None + + +def main(): + projects = load_json(PROJECTS_FILE) + workspace = load_json(WORKSPACE_JSON) + + if not projects: + print("ERROR: Cannot read projects.json") + sys.exit(0) + + items = projects.get("items", []) + satellites = (workspace or {}).get("satellites", {}) + + # --- VELOCITY --- + print("## VELOCITY (PAUL Projects)") + paul_projects = [] + for item in items: + paul = item.get("paul") + if paul and paul.get("is_paul_project") and paul.get("phase"): + lp_age = days_ago(paul.get("last_plan_completed_at") or paul.get("last_update")) + paul_projects.append({ + "id": item["id"], + "title": item["title"][:35], + "phase": f"{paul.get('completed_phases', '?')}/{paul.get('total_phases', '?')}", + "loop": paul.get("loop_position", "?"), + "last_plan_age": lp_age, + "handoff": paul.get("handoff", False), + "status": item.get("status"), + }) + + if paul_projects: + for p in sorted(paul_projects, key=lambda x: (x["last_plan_age"] or 0), reverse=True): + age_str = f"{p['last_plan_age']}d ago" if p["last_plan_age"] is not None else "never" + hf = " [HANDOFF]" if (isinstance(p["handoff"], dict) and p["handoff"].get("present")) or p["handoff"] is True else "" + print(f" {p['id']} {p['title']:35s} Phase {p['phase']:8s} {p['loop']:5s} plan: {age_str}{hf}") + else: + print(" No PAUL projects found") + print() + + # --- STALLS (active projects with plan age > 14d) --- + print("## STALLS (plan age > 14 days, not completed/deferred)") + stalls = [p for p in paul_projects + if p["last_plan_age"] is not None + and p["last_plan_age"] > 14 + and p["status"] not in ("completed", "deferred", "archived")] + if stalls: + for s in sorted(stalls, key=lambda x: x["last_plan_age"], reverse=True): + print(f" {s['id']} {s['title']:35s} STALLED {s['last_plan_age']}d") + else: + print(" No stalls detected") + print() + + # --- BLOCKING ANALYSIS --- + print("## BLOCKING ANALYSIS") + blocked = [i for i in items if i.get("blocked_by") and i.get("status") not in ("completed", "archived")] + if blocked: + # Group by blocker + blockers = defaultdict(list) + for item in blocked: + blockers[item["blocked_by"]].append(item) + + for blocker, items_blocked in blockers.items(): + rev_items = [i for i in items_blocked if i.get("revenue")] + rev_str = "" + if rev_items: + rev_str = f" | Revenue at risk: {', '.join(i['revenue']['amount'] for i in rev_items)}" + print(f" Blocker: {blocker}") + for i in items_blocked: + print(f" {i['id']} {i['title'][:40]}") + if rev_str: + print(f" {rev_str}") + print() + else: + print(" No blocked projects") + print() + + # --- DEPENDENCIES --- + print("## CROSS-PROJECT DEPENDENCIES") + has_deps = [i for i in items if i.get("dependencies")] + if has_deps: + for item in has_deps: + for dep in item["dependencies"]: + dep_project = next((i for i in items if i["id"] == dep["project_id"]), None) + dep_title = dep_project["title"][:30] if dep_project else dep["project_id"] + print(f" {item['id']} {item['title'][:30]} --{dep['type']}--> {dep_title}") + if dep.get("notes"): + print(f" Note: {dep['notes']}") + else: + print(" No cross-project dependencies defined") + print() + + # --- WORKLOAD BY CATEGORY --- + print("## WORKLOAD BY CATEGORY") + active = [i for i in items if i.get("status") not in ("backlog", "archived", "completed") and i.get("type") != "initiative"] + cats = defaultdict(int) + for item in active: + cats[item.get("category", "uncategorized")] += 1 + for cat, count in sorted(cats.items(), key=lambda x: -x[1]): + print(f" {cat}: {count} projects") + print() + + # --- REVENUE SUMMARY --- + print("## REVENUE EXPOSURE") + rev_projects = [i for i in items if i.get("revenue") and i.get("status") not in ("completed", "archived")] + if rev_projects: + for item in rev_projects: + rev = item["revenue"] + status = item.get("status", "?") + blocked_flag = " [BLOCKED]" if item.get("blocked_by") else "" + print(f" {item['id']} {item['title'][:35]} | {rev['amount']} ({rev['type']}){blocked_flag}") + else: + print(" No revenue projects active") + print() + + # --- HANDOFFS --- + print("## PENDING HANDOFFS") + handoff_sats = [(name, sat) for name, sat in satellites.items() if sat.get("handoff")] + if handoff_sats: + for name, sat in handoff_sats: + phase = sat.get("phase_name", "?") + print(f" {name}: Phase {phase} — has HANDOFF waiting") + else: + print(" No pending handoffs") + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"ERROR: {e}") + sys.exit(0) diff --git a/hooks/backlog-hook.py b/hooks/backlog-hook.py new file mode 100644 index 0000000..9d5a196 --- /dev/null +++ b/hooks/backlog-hook.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +BASE Hook v2: backlog-hook-v2.py +Source: .base/data/projects.json (APEX unified project management) +Output: compact summary grouped by priority +Filters: only items with status "backlog" + +Drop-in replacement for backlog-hook.py. Swap in settings.json when ready. +Legacy backlog-hook.py reads from .base/data/backlog.json (unchanged). +""" + +import sys +import json +from pathlib import Path +from datetime import date + +SURFACE_NAME = "backlog" + +HOOK_DIR = Path(__file__).resolve().parent +import os as _bh_os +import sys as _bh_sys +_bh_pd = _bh_os.environ.get("CLAUDE_PROJECT_DIR") +if _bh_pd and _bh_pd.strip(): + WORKSPACE_ROOT = Path(_bh_pd).resolve() +else: + WORKSPACE_ROOT = HOOK_DIR.parent.parent + if not (WORKSPACE_ROOT / ".base").is_dir(): + _bh_sys.stderr.write("[base-hook] CLAUDE_PROJECT_DIR unset and no .base/ at %s; hook is a no-op. Set CLAUDE_PROJECT_DIR.\n" % WORKSPACE_ROOT) +DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / "projects.json" + +BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. +Do NOT proactively mention these items unless: + - User explicitly asks (e.g., "what's in the backlog?", "what's queued?") + - A review_by date has passed AND user hasn't acknowledged it this session +For details on any item, use base_get_project(id).""" + +PRIORITY_ORDER = ["high", "medium", "low"] + +# Staleness thresholds (days since last update) +STALE_THRESHOLDS = { + "high": 7, + "medium": 14, + "low": 30, +} + + +def days_since_update(item): + """Calculate days since last update. Uses updated_at (ISO datetime).""" + ts = item.get("updated_at") or item.get("created_at") + if not ts: + return None + try: + d = date.fromisoformat(ts[:10]) + return (date.today() - d).days + except (ValueError, TypeError): + return None + + +def main(): + try: + input_data = json.loads(sys.stdin.read()) + except (json.JSONDecodeError, OSError): + pass + + if not DATA_FILE.exists(): + sys.exit(0) + + try: + data = json.loads(DATA_FILE.read_text()) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + items = data.get("items", []) + if not items: + sys.exit(0) + + # Filter: only backlog items + backlog_items = [i for i in items if i.get("status") == "backlog"] + if not backlog_items: + sys.exit(0) + + # Group by priority + groups = {} + for item in backlog_items: + p = item.get("priority", "medium") + groups.setdefault(p, []).append(item) + + lines = [] + for priority in PRIORITY_ORDER: + group = groups.get(priority, []) + if not group: + continue + lines.append(f"[{priority.upper()}]") + for item in group: + item_id = item.get("id", "?") + title = item.get("title", "untitled") + review_by = item.get("review_by") + entry = f"- [{item_id}] {title}" + if review_by: + entry += f" [review by: {review_by}]" + days = days_since_update(item) + threshold = STALE_THRESHOLDS.get(priority, 14) + if days is not None: + if days >= threshold: + entry += f" STALE: {days}d" + else: + entry += f" ({days}d ago)" + lines.append(entry) + + if lines: + count = len(backlog_items) + summary = "\n".join(lines) + print(f"""<{SURFACE_NAME}-awareness items="{count}"> +{summary} + +{BEHAVIOR_DIRECTIVE} +""") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/base-pulse-check.py b/hooks/base-pulse-check.py new file mode 100644 index 0000000..96b2565 --- /dev/null +++ b/hooks/base-pulse-check.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +BASE Hook v2: base-pulse-check-v2.py +Purpose: Workspace health check on session start. + Reads .base/data/state.json (pre-calculated drift, areas, groom config). + Much simpler than v1 which parsed STATE.md text + computed drift from file mtimes. +Triggers: UserPromptSubmit (session context) +Output: workspace health status or groom reminder + +Drop-in replacement for base-pulse-check.py. Swap in settings.json when ready. +Legacy base-pulse-check.py reads STATE.md + workspace.json (unchanged). +""" + +import sys +import json +from datetime import datetime, date +from pathlib import Path + +HOOK_DIR = Path(__file__).resolve().parent +import os as _bh_os +import sys as _bh_sys +_bh_pd = _bh_os.environ.get("CLAUDE_PROJECT_DIR") +if _bh_pd and _bh_pd.strip(): + WORKSPACE_ROOT = Path(_bh_pd).resolve() +else: + WORKSPACE_ROOT = HOOK_DIR.parent.parent + if not (WORKSPACE_ROOT / ".base").is_dir(): + _bh_sys.stderr.write("[base-hook] CLAUDE_PROJECT_DIR unset and no .base/ at %s; hook is a no-op. Set CLAUDE_PROJECT_DIR.\n" % WORKSPACE_ROOT) +BASE_DIR = WORKSPACE_ROOT / ".base" +STATE_FILE = BASE_DIR / "data" / "state.json" +PROJECTS_FILE = BASE_DIR / "data" / "projects.json" +CARL_DIR = WORKSPACE_ROOT / ".carl" +CARL_JSON = CARL_DIR / "carl.json" + + +def recalculate_drift(state): + """Recalculate drift indicators from live data and update state.json. + + This ensures drift score is always fresh on session start, not stale + from the last time base_update_drift was manually called. + """ + now = date.today() + + # Calculate indicators from projects.json + indicators = { + "active_age_days": 0, + "backlog_age_days": 0, + "backlog_past_review": 0, + "orphaned_sessions": 0, + "untracked_root_files": 0, + "stale_satellites": 0, + } + + if PROJECTS_FILE.exists(): + try: + projects = json.loads(PROJECTS_FILE.read_text()) + items = projects.get("items", []) + + # Active staleness: max days since update for active/in_progress/blocked/in_review projects + active_statuses = {"in_progress", "blocked", "in_review", "todo"} + active_ages = [] + backlog_ages = [] + past_review = 0 + + for item in items: + if item.get("type") != "project": + continue + + updated = item.get("updated_at") + if updated: + try: + updated_date = datetime.fromisoformat(updated).date() + age = (now - updated_date).days + except (ValueError, TypeError): + age = 0 + else: + age = 0 + + status = item.get("status", "") + if status in active_statuses: + active_ages.append(age) + elif status == "backlog": + backlog_ages.append(age) + + # Check review_by dates + review_by = item.get("review_by") + if review_by: + try: + review_date = date.fromisoformat(review_by) + if now > review_date: + past_review += 1 + except (ValueError, TypeError): + pass + + indicators["active_age_days"] = max(active_ages) if active_ages else 0 + indicators["backlog_age_days"] = max(backlog_ages) if backlog_ages else 0 + indicators["backlog_past_review"] = past_review + + except (json.JSONDecodeError, OSError): + pass + + # Stale satellites: check paul.json timestamps + satellites = state.get("satellites", {}) + stale_sats = 0 + for name, sat in satellites.items(): + sat_path = WORKSPACE_ROOT / sat.get("path", "") / ".paul" / "paul.json" + if sat_path.exists(): + try: + paul = json.loads(sat_path.read_text()) + ts = paul.get("timestamps", {}).get("updated_at") + if ts: + updated_date = datetime.fromisoformat(ts).date() + if (now - updated_date).days > 14: + stale_sats += 1 + except (json.JSONDecodeError, OSError, ValueError): + pass + indicators["stale_satellites"] = stale_sats + + # Compute score as sum of indicators + score = sum(v for v in indicators.values() if isinstance(v, (int, float))) + + # Write back to state + if "drift" not in state: + state["drift"] = {} + state["drift"]["score"] = score + state["drift"]["indicators"] = indicators + + try: + state["last_modified"] = datetime.now().isoformat() + STATE_FILE.write_text(json.dumps(state, indent=2)) + except OSError: + pass + + return state + + +def main(): + if not STATE_FILE.exists(): + sys.exit(0) + + try: + state = json.loads(STATE_FILE.read_text()) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + # Self-heal: recalculate drift from live data every session start + state = recalculate_drift(state) + + output_parts = [] + now = date.today() + + # Check groom overdue + groom = state.get("groom", {}) + next_due = groom.get("next_groom_due") + if next_due: + try: + due_date = date.fromisoformat(next_due) + if now > due_date: + last_groom = groom.get("last_groom", "unknown") + overdue_days = (now - due_date).days + output_parts.append( + f"BASE: Workspace groom overdue by {overdue_days} days " + f"(last groom: {last_groom}). " + f"Run /base:groom to maintain workspace health." + ) + except ValueError: + pass + + # Drift score and stale areas + drift = state.get("drift", {}) + drift_score = drift.get("score", 0) + areas = state.get("areas", {}) + stale_areas = [name for name, area in areas.items() if area.get("status") in ("stale", "critical")] + + if stale_areas: + output_parts.append( + f"BASE drift score: {drift_score} | Stale areas: {', '.join(stale_areas)}" + ) + elif drift_score == 0: + last_groom = groom.get("last_groom", "unknown") + output_parts.append( + f"BASE: Drift 0 | Last groom: {last_groom} | All areas current" + ) + + # CARL hygiene reminder + carl_hygiene = state.get("carl_hygiene", {}) + if carl_hygiene.get("proactive", False): + hygiene_cadence = {"weekly": 7, "bi-weekly": 14, "monthly": 30}.get( + carl_hygiene.get("cadence", "monthly"), 30 + ) + last_run = carl_hygiene.get("last_run") + if last_run: + try: + last_run_date = date.fromisoformat(last_run) + days_since = (now - last_run_date).days + if days_since > hygiene_cadence: + output_parts.append( + f"CARL hygiene overdue ({days_since}d since last run). Run /base:carl-hygiene" + ) + except ValueError: + output_parts.append("CARL hygiene: last_run date invalid. Run /base:carl-hygiene") + else: + output_parts.append("CARL hygiene never run. Run /base:carl-hygiene when ready") + + # Check staging proposals in carl.json + if CARL_JSON.exists(): + try: + carl_data = json.loads(CARL_JSON.read_text()) + pending = [p for p in carl_data.get("staging", []) if p.get("status") == "pending"] + if pending: + output_parts[-1] += f" | {len(pending)} staged proposals pending" + except (json.JSONDecodeError, OSError): + pass + + if output_parts: + print(f""" +{chr(10).join(output_parts)} +""") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..675ed20 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,56 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/psmm-injector.py\"" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/active-hook.py\"" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/backlog-hook.py\"" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/base-pulse-check.py\"" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/operator.py\"" + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/satellite-detection.py\"" + } + ] + } + ] + } +} diff --git a/hooks/operator.py b/hooks/operator.py new file mode 100644 index 0000000..7debaf8 --- /dev/null +++ b/hooks/operator.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +BASE Hook: operator.py +Source: .base/operator.json +Output: compact identity summary for alignment context +Controlled by: hook_active field in operator.json (true/false) +""" + +import json +from pathlib import Path + +HOOK_DIR = Path(__file__).resolve().parent +import os as _bh_os +import sys as _bh_sys +_bh_pd = _bh_os.environ.get("CLAUDE_PROJECT_DIR") +if _bh_pd and _bh_pd.strip(): + WORKSPACE_ROOT = Path(_bh_pd).resolve() +else: + WORKSPACE_ROOT = HOOK_DIR.parent.parent + if not (WORKSPACE_ROOT / ".base").is_dir(): + _bh_sys.stderr.write("[base-hook] CLAUDE_PROJECT_DIR unset and no .base/ at %s; hook is a no-op. Set CLAUDE_PROJECT_DIR.\n" % WORKSPACE_ROOT) +DATA_FILE = WORKSPACE_ROOT / ".base" / "operator.json" + + +def main(): + if not DATA_FILE.exists(): + return + + try: + data = json.loads(DATA_FILE.read_text()) + except (json.JSONDecodeError, IOError): + return + + # Check activation flag + if not data.get("hook_active", False): + return + + # Extract high-signal fields + north_star = data.get("north_star", {}).get("metric", "Not set") + timeframe = data.get("north_star", {}).get("timeframe", "") + deep_why = data.get("deep_why", {}).get("statement", "Not set") + values = [v.get("value", "") for v in data.get("key_values", {}).get("values", [])] + vision = data.get("surface_vision", {}).get("summary", "Not set") + pitch = data.get("elevator_pitch", {}).get("pitch", "Not set") + + values_str = ", ".join(values) if values else "Not set" + star_str = f"{north_star} ({timeframe})" if timeframe else north_star + + output = f""" +North Star: {star_str} +Deep Why: {deep_why} +Values: {values_str} +Vision: {vision} +Pitch: {pitch} +""" + + print(output) + + +if __name__ == "__main__": + main() diff --git a/hooks/psmm-injector.py b/hooks/psmm-injector.py new file mode 100644 index 0000000..2aa93c3 --- /dev/null +++ b/hooks/psmm-injector.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Hook: psmm-injector.py +Purpose: Per-Session Meta Memory — inject ephemeral session observations + into every prompt so they stay hot in long sessions (1M window). + + Uses a single psmm.json file with session-keyed entries. + Each session gets its own array keyed by Claude Code session UUID. + Stale sessions are NOT auto-cleaned — that's the operator's job + via CARL hygiene / BASE drift detection. + +Triggers: UserPromptSubmit +Output: Current session's PSMM entries as system context, or silent if empty. +""" + +import os +import sys +import json +from pathlib import Path + +HOOK_DIR = Path(__file__).resolve().parent +WORKSPACE_ROOT = HOOK_DIR.parent.parent + +_project_dir = os.environ.get("CLAUDE_PROJECT_DIR") +if _project_dir: + PSMM_FILE = Path(_project_dir) / ".base" / "data" / "psmm.json" +else: + PSMM_FILE = Path(".").resolve() / ".base" / "data" / "psmm.json" + if not PSMM_FILE.exists(): + PSMM_FILE = WORKSPACE_ROOT / ".base" / "data" / "psmm.json" + + +def main(): + # Get session_id from hook input + try: + input_data = json.loads(sys.stdin.read()) + session_id = input_data.get("session_id", "") + except (json.JSONDecodeError, OSError): + session_id = "" + + if not session_id or not PSMM_FILE.exists(): + sys.exit(0) + + try: + data = json.loads(PSMM_FILE.read_text()) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + sessions = data.get("sessions", {}) + session = sessions.get(session_id) + + if not session or not session.get("entries"): + sys.exit(0) + + # Build output from this session's entries + entries = session["entries"] + lines = [] + for entry in entries: + entry_type = entry.get("type", "NOTE") + text = entry.get("text", "") + timestamp = entry.get("timestamp", "") + lines.append(f"- [{timestamp}] {entry_type}: {text}") + + if lines: + created = session.get("created", "unknown") + count = len(entries) + print(f""" +{chr(10).join(lines)} +""") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/satellite-detection.py b/hooks/satellite-detection.py new file mode 100644 index 0000000..8ea3f64 --- /dev/null +++ b/hooks/satellite-detection.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +""" +Hook: satellite-detection.py +Purpose: Scans the workspace recursively for .paul/paul.json files, + auto-registers new satellites, and syncs paul.json state to + workspace.json and projects.json. +Triggers: SessionStart — runs once when Claude Code starts a session. +Output: block if new satellites registered, silent otherwise. + +Sync flow (paul.json → workspace.json → projects.json): + 1. Discover paul.json files across workspace + 2. Register new satellites (existing behavior) + 3. Sync paul.json state to workspace.json satellite entries + 4. Cross-check projects.json: update paul field on matching projects + Respects satellite.sync: false as opt-out for steps 3-4. +""" + +import sys +import json +from datetime import datetime +from pathlib import Path + +# Workspace root — find .base/ relative to this hook's location +HOOK_DIR = Path(__file__).resolve().parent +import os as _bh_os +import sys as _bh_sys +_bh_pd = _bh_os.environ.get("CLAUDE_PROJECT_DIR") +if _bh_pd and _bh_pd.strip(): + WORKSPACE_ROOT = Path(_bh_pd).resolve() +else: + WORKSPACE_ROOT = HOOK_DIR.parent.parent + if not (WORKSPACE_ROOT / ".base").is_dir(): + _bh_sys.stderr.write("[base-hook] CLAUDE_PROJECT_DIR unset and no .base/ at %s; hook is a no-op. Set CLAUDE_PROJECT_DIR.\n" % WORKSPACE_ROOT) # hooks/ -> .base/ -> workspace +BASE_DIR = WORKSPACE_ROOT / ".base" +MANIFEST_FILE = BASE_DIR / "workspace.json" +PROJECTS_FILE = BASE_DIR / "data" / "projects.json" + + +def has_hidden_component(path: Path, workspace_root: Path) -> bool: + """ + Return True if any component of path (relative to workspace_root) starts with '.', + excluding '.paul' itself (which is the expected target directory). + """ + try: + rel = path.relative_to(workspace_root) + except ValueError: + return True # Can't relativize — skip it + return any(part.startswith(".") and part != ".paul" for part in rel.parts) + + +def find_paul_json_files(workspace_root: Path) -> list[Path]: + """ + Recursively scan workspace_root for .paul/paul.json files. + Skips any path that has a hidden directory component (starts with '.'). + """ + results = [] + try: + for paul_json in workspace_root.rglob(".paul/paul.json"): + if not has_hidden_component(paul_json, workspace_root): + results.append(paul_json) + except (OSError, PermissionError): + pass + return results + + +def should_sync(paul_data: dict) -> bool: + """Check if this satellite opts into sync. Default: True.""" + satellite = paul_data.get("satellite", {}) + return satellite.get("sync", True) + + +def sync_to_workspace(satellites: dict, paul_data: dict, name: str) -> bool: + """Sync paul.json state to workspace.json satellite entry. Returns True if changed.""" + if name not in satellites: + return False + + sat = satellites[name] + changed = False + + phase = paul_data.get("phase", {}) + loop = paul_data.get("loop", {}) + handoff = paul_data.get("handoff", {}) + + updates = { + "phase_name": phase.get("name"), + "phase_number": phase.get("number"), + "phase_status": phase.get("status"), + "loop_position": loop.get("position"), + "handoff": handoff.get("present", False), + "last_plan_completed_at": paul_data.get("last_plan_completed_at"), + "next_action": paul_data.get("next_action"), + } + + for key, value in updates.items(): + if sat.get(key) != value: + sat[key] = value + changed = True + + return changed + + +def build_paul_field(paul_data: dict, name: str, sat_path: str) -> dict: + """Build a standardized paul field from paul.json data.""" + phase = paul_data.get("phase", {}) + loop = paul_data.get("loop", {}) + handoff = paul_data.get("handoff", {}) + milestone = paul_data.get("milestone", {}) + timestamps = paul_data.get("timestamps", {}) + + completed = phase.get("number", 1) if phase.get("status") == "complete" else max(0, (phase.get("number", 1) or 1) - 1) + + return { + "is_paul_project": True, + "satellite_name": name, + "location": sat_path.rstrip("/") + "/", + "milestone": milestone.get("name"), + "phase": phase.get("name"), + "phase_name": phase.get("name"), + "loop_position": loop.get("position"), + "last_update": timestamps.get("updated_at"), + "handoff": handoff.get("present", False), + "handoff_path": handoff.get("path"), + "completed_phases": completed, + "total_phases": phase.get("total"), + "last_plan_completed_at": paul_data.get("last_plan_completed_at"), + } + + +def find_project_by_path(items: list, sat_path: str): + """Find project by location path (flexible trailing slash matching).""" + normalized = sat_path.rstrip("/") + for item in items: + loc = (item.get("location") or "").rstrip("/") + if loc == normalized: + return item + return None + + +def sync_to_projects(paul_data: dict, name: str, sat_path: str, projects_data: dict) -> str: + """Sync paul.json state to matching project in projects.json. + Returns: 'updated', 'created', or 'none'.""" + items = projects_data.get("items", []) + + # Match by satellite_name first, then by path + project = None + for item in items: + paul_field = item.get("paul") + if paul_field and paul_field.get("satellite_name") == name: + project = item + break + if not project: + project = find_project_by_path(items, sat_path) + + paul_field = build_paul_field(paul_data, name, sat_path) + + if project: + # Update existing — merge paul field + if not project.get("paul"): + project["paul"] = {} + project["paul"].update(paul_field) + project["updated_at"] = datetime.now().isoformat() + return "updated" + + # Auto-create project entry + max_num = 0 + for item in items: + match = (item.get("id") or "").replace("PRJ-", "") + try: + num = int(match) + if num > max_num: + max_num = num + except ValueError: + pass + + new_id = f"PRJ-{max_num + 1:03d}" + title = paul_data.get("project", {}).get("title") or name + now = datetime.now().isoformat() + + items.append({ + "id": new_id, + "title": title, + "type": "project", + "parent_id": None, + "status": "in_progress", + "priority": "medium", + "category": "internal", + "assignees": [], + "start_date": None, + "due_date": None, + "created_at": now, + "updated_at": now, + "location": sat_path.rstrip("/") + "/", + "blocked_by": None, + "next": None, + "notes": [], + "tags": [], + "paul": paul_field, + "relations": [], + "description": None, + }) + return "created" + + +def main(): + # Skip if BASE is not installed + if not BASE_DIR.exists() or not MANIFEST_FILE.exists(): + sys.exit(0) + + try: + with open(MANIFEST_FILE, "r") as f: + manifest = json.load(f) + except (json.JSONDecodeError, OSError): + sys.exit(0) + + satellites = manifest.get("satellites", {}) + new_registrations = [] + workspace_changed = False + projects_changed = False + + # Load projects.json for cross-check (if it exists) + projects_data = None + if PROJECTS_FILE.exists(): + try: + with open(PROJECTS_FILE, "r") as f: + projects_data = json.load(f) + except (json.JSONDecodeError, OSError): + projects_data = None + + paul_files = find_paul_json_files(WORKSPACE_ROOT) + + # Collect paul data for sync pass + paul_registry = {} # name → paul_data + + for paul_json_path in paul_files: + try: + with open(paul_json_path, "r") as f: + paul_data = json.load(f) + except (json.JSONDecodeError, OSError): + continue # Malformed or unreadable — skip silently + + name = paul_data.get("name") + if not name: + continue # No name field — skip + + paul_registry[name] = paul_data + + # Read last_activity from paul.json timestamps (if present) + last_activity = paul_data.get("timestamps", {}).get("updated_at") + + if name in satellites: + # Already registered — refresh last_activity if available + if last_activity and satellites[name].get("last_activity") != last_activity: + satellites[name]["last_activity"] = last_activity + workspace_changed = True + continue + + # New satellite — derive relative path + project_dir = paul_json_path.parent.parent + try: + rel_path = str(project_dir.relative_to(WORKSPACE_ROOT)) + except ValueError: + continue # Can't relativize — skip + + # Build registration entry + entry = { + "path": rel_path, + "engine": "paul", + "state": f"{rel_path}/.paul/STATE.md", + "registered": datetime.now().strftime("%Y-%m-%d"), + "groom_check": True, + } + if last_activity: + entry["last_activity"] = last_activity + + satellites[name] = entry + new_registrations.append(name) + workspace_changed = True + + # --- Sync pass: paul.json → workspace.json + projects.json --- + for name, paul_data in paul_registry.items(): + if not should_sync(paul_data): + continue # Opt-out — skip sync + + # Sync to workspace.json + if sync_to_workspace(satellites, paul_data, name): + workspace_changed = True + + # Sync to projects.json + if projects_data: + sat_path = satellites.get(name, {}).get("path", "") + result = sync_to_projects(paul_data, name, sat_path, projects_data) + if result in ("updated", "created"): + projects_changed = True + + # Write workspace.json if changed + if workspace_changed: + try: + manifest["satellites"] = satellites + with open(MANIFEST_FILE, "w") as f: + json.dump(manifest, f, indent=2) + f.write("\n") + except OSError: + pass # Write failed — silent + + # Write projects.json if changed + if projects_changed and projects_data: + try: + projects_data["last_modified"] = datetime.now().isoformat() + with open(PROJECTS_FILE, "w") as f: + json.dump(projects_data, f, indent=2) + f.write("\n") + except OSError: + pass # Write failed — silent + + # Output only for new registrations + if new_registrations: + names_str = ", ".join(new_registrations) + n = len(new_registrations) + print(f"\nAuto-registered {n} new satellite(s): {names_str}\n") + + sys.exit(0) + + +if __name__ == "__main__": + try: + main() + except Exception: + sys.exit(0) diff --git a/mcp/index.js b/mcp/index.js new file mode 100644 index 0000000..3e3bbbf --- /dev/null +++ b/mcp/index.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node +/** + * BASE MCP — Workspace Orchestration Server + * Builder's Automated State Engine + * + * Project management, entities, state tracking, operator profile, and PSMM. + * All data stored as JSON in .base/data/. + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'node:fs'; + +// Tool group imports +import { TOOLS as projectTools, handleTool as handleProject } from './tools/projects.js'; +import { TOOLS as stateTools, handleTool as handleState } from './tools/state.js'; +import { TOOLS as entityTools, handleTool as handleEntity } from './tools/entities.js'; +import { TOOLS as operatorTools, handleTool as handleOperator } from './tools/operator.js'; +import { TOOLS as psmmTools, handleTool as handlePsmm } from './tools/psmm.js'; +import { TOOLS as satelliteTools, handleTool as handleSatellite } from './tools/satellite.js'; + +// ============================================================ +// CONFIGURATION +// ============================================================ + +// Resolve workspace from this file's location: base-mcp/ → .base/ → workspace root +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const WORKSPACE_PATH = (() => { const c = process.env.CLAUDE_PROJECT_DIR; if (c && c.trim()) return path.resolve(c.trim()); const real = (p) => { try { return fs.realpathSync(path.resolve(p)); } catch { return path.resolve(p); } }; const h = process.env.HOME || process.env.USERPROFILE; const home = h ? real(h) : null; const f = real(path.resolve(__dirname, '../..')); const vendored = path.basename(__dirname) === 'base-mcp' && path.basename(path.dirname(__dirname)) === '.base'; if (vendored && f !== home) return f; const cwd = real(process.cwd()); if (cwd !== home) return cwd; throw new Error('[BASE] Refusing to start: CLAUDE_PROJECT_DIR is unset and no project workspace could be resolved (cwd and install location are your home directory; refusing to read a global ~/.base store). Set CLAUDE_PROJECT_DIR or launch base-mcp from the project root.'); })(); + +function debugLog(...args) { + console.error('[BASE]', new Date().toISOString(), ...args); +} + +// ============================================================ +// TOOL REGISTRY +// ============================================================ + +const ALL_TOOLS = [...projectTools, ...stateTools, ...entityTools, ...operatorTools, ...psmmTools, ...satelliteTools]; + +// Build handler lookup: tool name → handler function +const TOOL_HANDLERS = {}; +for (const tool of projectTools) TOOL_HANDLERS[tool.name] = handleProject; +for (const tool of stateTools) TOOL_HANDLERS[tool.name] = handleState; +for (const tool of entityTools) TOOL_HANDLERS[tool.name] = handleEntity; +for (const tool of operatorTools) TOOL_HANDLERS[tool.name] = handleOperator; +for (const tool of psmmTools) TOOL_HANDLERS[tool.name] = handlePsmm; +for (const tool of satelliteTools) TOOL_HANDLERS[tool.name] = handleSatellite; + +// ============================================================ +// MCP SERVER +// ============================================================ + +const server = new Server({ + name: "base-mcp", + version: "2.0.0", +}, { + capabilities: { + tools: {}, + }, +}); + +debugLog('BASE MCP Server initialized'); +debugLog('Workspace:', WORKSPACE_PATH); +debugLog('Tool groups: projects (%d), state (%d), entities (%d), operator (%d), psmm (%d), satellite (%d)', + projectTools.length, stateTools.length, entityTools.length, operatorTools.length, psmmTools.length, satelliteTools.length); +debugLog('Total tools:', ALL_TOOLS.length); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + debugLog('List tools request'); + return { tools: ALL_TOOLS }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + debugLog('Call tool:', name); + + try { + const handler = TOOL_HANDLERS[name]; + if (!handler) { + throw new Error(`Unknown tool: ${name}`); + } + + const result = await handler(name, args || {}, WORKSPACE_PATH); + + if (result === null) { + throw new Error(`Tool ${name} returned null — handler mismatch`); + } + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + isError: false, + }; + } catch (error) { + debugLog('Error:', error.message); + return { + content: [{ type: "text", text: `Error: ${error.message}` }], + isError: true, + }; + } +}); + +// ============================================================ +// RUN +// ============================================================ + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("BASE MCP Server running on stdio"); +} + +try { + await runServer(); +} catch (error) { + console.error("Fatal error:", error); + process.exit(1); +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000..f873e13 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,10 @@ +{ + "name": "base-mcp", + "version": "1.0.0", + "description": "BASE Surface CRUD Server - Generic operations for registered data surfaces", + "type": "module", + "main": "index.js", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } +} diff --git a/mcp/tools/entities.js b/mcp/tools/entities.js new file mode 100644 index 0000000..0f9e983 --- /dev/null +++ b/mcp/tools/entities.js @@ -0,0 +1,228 @@ +/** + * BASE Entities — CRUD for entities.json + * People and organizations with relational links to projects + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { validateSurface } from './validate.js'; + +function debugLog(...args) { + console.error('[BASE:entities]', new Date().toISOString(), ...args); +} + +// ============================================================ +// HELPERS +// ============================================================ + +function getEntitiesPath(workspacePath) { + return join(workspacePath, '.base', 'data', 'entities.json'); +} + +function readEntities(workspacePath) { + const filepath = getEntitiesPath(workspacePath); + if (!existsSync(filepath)) { + return { version: 1, last_modified: null, entities: [] }; + } + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch (error) { + debugLog('Error reading entities.json:', error.message); + return { version: 1, last_modified: null, entities: [] }; + } +} + +function writeEntities(workspacePath, data) { + const filepath = getEntitiesPath(workspacePath); + data.last_modified = new Date().toISOString(); + validateSurface('entities', data); + writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); +} + +function generateEntityId(entities) { + let max = 0; + for (const entity of entities) { + const match = (entity.id || '').match(/^ENT-(\d+)$/); + if (match) { + const num = parseInt(match[1], 10); + if (num > max) max = num; + } + } + return `ENT-${String(max + 1).padStart(3, '0')}`; +} + +function formatTimestamp() { + return new Date().toISOString(); +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_list_entities", + description: "List all entities with optional type filter (person/organization).", + inputSchema: { + type: "object", + properties: { + type: { type: "string", enum: ["person", "organization"], description: "Filter by entity type" } + }, + required: [] + } + }, + { + name: "base_add_entity", + description: "Add a new person or organization entity. Auto-generates ENT-NNN ID.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Display name" }, + type: { type: "string", enum: ["person", "organization"], description: "Entity type" }, + role: { type: "string", description: "Primary role (owner, client, partner, contractor, team member, employer)" } + }, + required: ["name", "type"] + } + }, + { + name: "base_update_entity", + description: "Update an entity's fields by ID. Shallow merge — only specified fields updated.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Entity ID (e.g., 'ENT-001')" }, + data: { type: "object", description: "Fields to update" } + }, + required: ["id", "data"] + } + }, + { + name: "base_link_entity", + description: "Add a relation between an entity and a project item. Avoids duplicate relations.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Entity ID (e.g., 'ENT-001')" }, + project_id: { type: "string", description: "Project item ID (e.g., 'PRJ-003')" }, + relationship: { type: "string", description: "Relationship type (stakeholder, client, owner, partner, contractor, assignee)" } + }, + required: ["id", "project_id", "relationship"] + } + } +]; + +// ============================================================ +// TOOL HANDLERS +// ============================================================ + +function handleListEntities(args, workspacePath) { + debugLog('Listing entities'); + const data = readEntities(workspacePath); + let entities = data.entities; + + if (args.type) { + entities = entities.filter(e => e.type === args.type); + } + + return { entities, count: entities.length, total: data.entities.length }; +} + +function handleAddEntity(args, workspacePath) { + const { name, type } = args; + if (!name) throw new Error('Missing required parameter: name'); + if (!type) throw new Error('Missing required parameter: type'); + + const data = readEntities(workspacePath); + const now = formatTimestamp(); + const id = generateEntityId(data.entities); + + const entity = { + id, + name, + type, + role: args.role || null, + relations: [], + notes: [], + created_at: now, + updated_at: now + }; + + data.entities.push(entity); + writeEntities(workspacePath, data); + + debugLog(`Added ${type} ${id}: ${name}`); + return entity; +} + +function handleUpdateEntity(args, workspacePath) { + const { id, data: updateData } = args; + if (!id) throw new Error('Missing required parameter: id'); + if (!updateData) throw new Error('Missing required parameter: data'); + + const data = readEntities(workspacePath); + const index = data.entities.findIndex(e => e.id === id); + if (index === -1) { + throw new Error(`Entity "${id}" not found. Available: ${data.entities.map(e => e.id).join(', ') || 'none'}`); + } + + data.entities[index] = { + ...data.entities[index], + ...updateData, + id, // Prevent ID overwrite + updated_at: formatTimestamp() + }; + + writeEntities(workspacePath, data); + + debugLog(`Updated ${id}`); + return data.entities[index]; +} + +function handleLinkEntity(args, workspacePath) { + const { id, project_id, relationship } = args; + if (!id) throw new Error('Missing required parameter: id'); + if (!project_id) throw new Error('Missing required parameter: project_id'); + if (!relationship) throw new Error('Missing required parameter: relationship'); + + const data = readEntities(workspacePath); + const index = data.entities.findIndex(e => e.id === id); + if (index === -1) { + throw new Error(`Entity "${id}" not found`); + } + + const entity = data.entities[index]; + if (!entity.relations) entity.relations = []; + + // Check for duplicate + const exists = entity.relations.some(r => r.project_id === project_id && r.relationship === relationship); + if (exists) { + return { already_linked: true, id, project_id, relationship, message: 'Relation already exists' }; + } + + entity.relations.push({ project_id, relationship }); + entity.updated_at = formatTimestamp(); + + writeEntities(workspacePath, data); + + debugLog(`Linked ${id} → ${project_id} (${relationship})`); + return { id, project_id, relationship, relations_count: entity.relations.length }; +} + +// ============================================================ +// HANDLER DISPATCH +// ============================================================ + +export function handleTool(name, args, workspacePath) { + switch (name) { + case 'base_list_entities': + return handleListEntities(args, workspacePath); + case 'base_add_entity': + return handleAddEntity(args, workspacePath); + case 'base_update_entity': + return handleUpdateEntity(args, workspacePath); + case 'base_link_entity': + return handleLinkEntity(args, workspacePath); + default: + return null; + } +} diff --git a/mcp/tools/operator.js b/mcp/tools/operator.js new file mode 100644 index 0000000..8035411 --- /dev/null +++ b/mcp/tools/operator.js @@ -0,0 +1,106 @@ +/** + * Operator CRUD tools for .base/operator.json + * Read, update sections, toggle hook activation + */ + +import fs from 'fs'; +import path from 'path'; + +function debugLog(...args) { + console.error('[BASE:operator]', new Date().toISOString(), ...args); +} + +function getOperatorPath(workspacePath) { + return path.join(workspacePath, '.base', 'operator.json'); +} + +function readOperator(workspacePath) { + const filepath = getOperatorPath(workspacePath); + try { + return JSON.parse(fs.readFileSync(filepath, 'utf8')); + } catch (error) { + return null; + } +} + +function writeOperator(workspacePath, data) { + const filepath = getOperatorPath(workspacePath); + data.last_updated = new Date().toISOString(); + fs.writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n'); +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_get_operator", + description: "Read the full operator profile from .base/operator.json. Returns identity, deep why, north star, values, pitch, vision.", + inputSchema: { type: "object", properties: {}, required: [] } + }, + { + name: "base_update_operator", + description: "Update a specific section of the operator profile. Sections: deep_why, north_star, key_values, elevator_pitch, surface_vision, extensions, hook_active. Pass the full section object to replace it.", + inputSchema: { + type: "object", + properties: { + section: { type: "string", description: "Section to update (deep_why, north_star, key_values, elevator_pitch, surface_vision, extensions, hook_active)" }, + data: { description: "New section data (object for sections, boolean for hook_active)" } + }, + required: ["section", "data"] + } + } +]; + +// ============================================================ +// TOOL HANDLERS +// ============================================================ + +async function handleGetOperator(_args, workspacePath) { + const data = readOperator(workspacePath); + if (!data) { + return { error: "No operator.json found. Run /base:orientation to create one." }; + } + return data; +} + +async function handleUpdateOperator(args, workspacePath) { + const { section, data: sectionData } = args; + if (!section) throw new Error('Missing required parameter: section'); + if (sectionData === undefined) throw new Error('Missing required parameter: data'); + + const validSections = ['deep_why', 'north_star', 'key_values', 'elevator_pitch', 'surface_vision', 'extensions', 'hook_active']; + if (!validSections.includes(section)) { + throw new Error(`Invalid section "${section}". Valid: ${validSections.join(', ')}`); + } + + const operator = readOperator(workspacePath); + if (!operator) { + throw new Error('No operator.json found. Run /base:orientation to create one.'); + } + + debugLog('Updating operator section:', section); + + if (section === 'hook_active') { + operator.hook_active = !!sectionData; + } else { + operator[section] = sectionData; + } + + writeOperator(workspacePath, operator); + + return { + section, + updated: true, + hook_active: operator.hook_active + }; +} + +export async function handleTool(name, args, workspacePath) { + switch (name) { + case 'base_get_operator': return handleGetOperator(args, workspacePath); + case 'base_update_operator': return handleUpdateOperator(args, workspacePath); + default: return null; + } +} diff --git a/mcp/tools/projects.js b/mcp/tools/projects.js new file mode 100644 index 0000000..8edc91f --- /dev/null +++ b/mcp/tools/projects.js @@ -0,0 +1,324 @@ +/** + * BASE Projects — Hierarchy-aware CRUD for projects.json + * Supports Initiative > Project > Task with auto-ID by type + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { validateSurface } from './validate.js'; + +function debugLog(...args) { + console.error('[BASE:projects]', new Date().toISOString(), ...args); +} + +// ============================================================ +// HELPERS +// ============================================================ + +const TYPE_PREFIX = { initiative: 'INI', project: 'PRJ', task: 'TSK' }; + +function getProjectsPath(workspacePath) { + return join(workspacePath, '.base', 'data', 'projects.json'); +} + +function readProjects(workspacePath) { + const filepath = getProjectsPath(workspacePath); + if (!existsSync(filepath)) { + return { version: 1, workspace: '', last_modified: null, categories: [], items: [], archived: [] }; + } + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch (error) { + debugLog('Error reading projects.json:', error.message); + return { version: 1, workspace: '', last_modified: null, categories: [], items: [], archived: [] }; + } +} + +function writeProjects(workspacePath, data) { + const filepath = getProjectsPath(workspacePath); + data.last_modified = new Date().toISOString(); + validateSurface('projects', data); + writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); +} + +function generateProjectId(type, items) { + const prefix = TYPE_PREFIX[type]; + if (!prefix) throw new Error(`Invalid type: ${type}. Valid: initiative, project, task`); + + let max = 0; + for (const item of items) { + const match = (item.id || '').match(new RegExp(`^${prefix}-(\\d+)$`)); + if (match) { + const num = parseInt(match[1], 10); + if (num > max) max = num; + } + } + return `${prefix}-${String(max + 1).padStart(3, '0')}`; +} + +function formatTimestamp() { + return new Date().toISOString(); +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_list_projects", + description: "List/filter project items. Supports filtering by type (initiative/project/task), status, priority, parent_id, and category.", + inputSchema: { + type: "object", + properties: { + type: { type: "string", enum: ["initiative", "project", "task"], description: "Filter by hierarchy level" }, + status: { type: "string", description: "Filter by status (backlog, todo, in_progress, blocked, in_review, completed, deferred, archived)" }, + priority: { type: "string", description: "Filter by priority (urgent, high, medium, low, ongoing)" }, + parent_id: { type: "string", description: "Filter by parent item ID" }, + category: { type: "string", description: "Filter by category" } + }, + required: [] + } + }, + { + name: "base_get_project", + description: "Get a single project item by ID. Returns the full item object.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Item ID (e.g., 'PRJ-001', 'INI-002', 'TSK-005')" } + }, + required: ["id"] + } + }, + { + name: "base_add_project", + description: "Add a new initiative, project, or task. Auto-generates ID by type (INI-NNN, PRJ-NNN, TSK-NNN). Sets created_at and updated_at.", + inputSchema: { + type: "object", + properties: { + type: { type: "string", enum: ["initiative", "project", "task"], description: "Hierarchy level" }, + title: { type: "string", description: "Item title" }, + parent_id: { type: "string", description: "Parent item ID (null for top-level initiatives)" }, + status: { type: "string", enum: ["backlog", "todo", "in_progress", "blocked", "in_review", "completed", "deferred"], description: "Initial status (default: todo)" }, + priority: { type: "string", enum: ["urgent", "high", "medium", "low", "ongoing"], description: "Priority level (default: medium)" }, + category: { type: "string", description: "Category from workspace categories list" }, + assignees: { type: "array", items: { type: "string" }, description: "Entity IDs to assign" }, + description: { type: "string", description: "Extended description" }, + location: { type: "string", description: "Workspace-relative path" }, + due_date: { type: "string", description: "ISO date deadline" }, + tags: { type: "array", items: { type: "string" }, description: "Free-form tags" } + }, + required: ["type", "title"] + } + }, + { + name: "base_update_project", + description: "Update an existing item's fields by ID. Shallow merge — only specified fields updated, others preserved.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Item ID to update" }, + data: { type: "object", description: "Fields to update (shallow merge)" } + }, + required: ["id", "data"] + } + }, + { + name: "base_archive_project", + description: "Archive an item by ID — moves from items[] to archived[] with outcome and timestamp.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Item ID to archive" }, + outcome: { type: "string", description: "What happened (shipped, killed, absorbed, etc.)" } + }, + required: ["id", "outcome"] + } + }, + { + name: "base_search_projects", + description: "Search project items by keyword. Case-insensitive substring match across title, description, notes, and tags.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query (case-insensitive)" } + }, + required: ["query"] + } + } +]; + +// ============================================================ +// TOOL HANDLERS +// ============================================================ + +function handleListProjects(args, workspacePath) { + const data = readProjects(workspacePath); + let items = data.items; + + if (args.type) items = items.filter(i => i.type === args.type); + if (args.status) items = items.filter(i => i.status === args.status); + if (args.priority) items = items.filter(i => i.priority === args.priority); + if (args.parent_id) items = items.filter(i => i.parent_id === args.parent_id); + if (args.category) items = items.filter(i => i.category === args.category); + + return { items, count: items.length, total: data.items.length }; +} + +function handleGetProject(args, workspacePath) { + const { id } = args; + if (!id) throw new Error('Missing required parameter: id'); + + const data = readProjects(workspacePath); + const item = data.items.find(i => i.id === id); + if (!item) { + throw new Error(`Item "${id}" not found. Available IDs: ${data.items.map(i => i.id).slice(0, 20).join(', ')}${data.items.length > 20 ? '...' : ''}`); + } + return item; +} + +function handleAddProject(args, workspacePath) { + const { type, title } = args; + if (!type) throw new Error('Missing required parameter: type'); + if (!title) throw new Error('Missing required parameter: title'); + + const data = readProjects(workspacePath); + const now = formatTimestamp(); + const id = generateProjectId(type, data.items); + + const newItem = { + id, + title, + type, + parent_id: args.parent_id || null, + status: args.status || 'todo', + priority: args.priority || 'medium', + category: args.category || null, + assignees: args.assignees || [], + start_date: null, + due_date: args.due_date || null, + created_at: now, + updated_at: now, + location: args.location || null, + blocked_by: null, + next: null, + notes: [], + tags: args.tags || [], + paul: null, + relations: [], + description: args.description || null + }; + + data.items.push(newItem); + writeProjects(workspacePath, data); + + debugLog(`Added ${type} ${id}: ${title}`); + return newItem; +} + +function handleUpdateProject(args, workspacePath) { + const { id, data: updateData } = args; + if (!id) throw new Error('Missing required parameter: id'); + if (!updateData) throw new Error('Missing required parameter: data'); + + const data = readProjects(workspacePath); + const index = data.items.findIndex(i => i.id === id); + if (index === -1) { + throw new Error(`Item "${id}" not found`); + } + + data.items[index] = { + ...data.items[index], + ...updateData, + id, // Prevent ID overwrite + updated_at: formatTimestamp() + }; + + writeProjects(workspacePath, data); + + debugLog(`Updated ${id}`); + return data.items[index]; +} + +function handleArchiveProject(args, workspacePath) { + const { id, outcome } = args; + if (!id) throw new Error('Missing required parameter: id'); + if (!outcome) throw new Error('Missing required parameter: outcome'); + + const data = readProjects(workspacePath); + const index = data.items.findIndex(i => i.id === id); + if (index === -1) { + throw new Error(`Item "${id}" not found`); + } + + const [item] = data.items.splice(index, 1); + const now = formatTimestamp(); + + if (!data.archived) data.archived = []; + data.archived.push({ + id: item.id, + title: item.title, + outcome, + date: now.split('T')[0], + archived_at: now + }); + + writeProjects(workspacePath, data); + + debugLog(`Archived ${id}: ${outcome}`); + return { id, title: item.title, outcome, archived_at: now }; +} + +function handleSearchProjects(args, workspacePath) { + const { query } = args; + if (!query) throw new Error('Missing required parameter: query'); + + const data = readProjects(workspacePath); + const queryLower = query.toLowerCase(); + const results = []; + + for (const item of data.items) { + const searchFields = [ + item.title, + item.description, + ...(item.tags || []), + ...(item.notes || []).map(n => n.text) + ].filter(Boolean).join(' ').toLowerCase(); + + if (searchFields.includes(queryLower)) { + results.push({ + id: item.id, + title: item.title, + type: item.type, + status: item.status, + priority: item.priority + }); + } + } + + return { results, count: results.length, query }; +} + +// ============================================================ +// HANDLER DISPATCH +// ============================================================ + +export function handleTool(name, args, workspacePath) { + switch (name) { + case 'base_list_projects': + return handleListProjects(args, workspacePath); + case 'base_get_project': + return handleGetProject(args, workspacePath); + case 'base_add_project': + return handleAddProject(args, workspacePath); + case 'base_update_project': + return handleUpdateProject(args, workspacePath); + case 'base_archive_project': + return handleArchiveProject(args, workspacePath); + case 'base_search_projects': + return handleSearchProjects(args, workspacePath); + default: + return null; + } +} diff --git a/mcp/tools/psmm.js b/mcp/tools/psmm.js new file mode 100644 index 0000000..1fbc040 --- /dev/null +++ b/mcp/tools/psmm.js @@ -0,0 +1,206 @@ +/** + * BASE PSMM — Per-Session Meta Memory tools + * Tracks significant meta moments across sessions. + * The injection hook (psmm-injector.py) re-injects entries into context every prompt. + * CARL connects only for graduation: PSMM entries can be staged as CARL rule proposals. + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +const VALID_TYPES = ['DECISION', 'CORRECTION', 'SHIFT', 'INSIGHT', 'COMMITMENT']; + +function debugLog(...args) { + console.error('[BASE:psmm]', new Date().toISOString(), ...args); +} + +function getPsmmPath(workspacePath) { + return join(workspacePath, '.base', 'data', 'psmm.json'); +} + +function readPsmm(workspacePath) { + const filepath = getPsmmPath(workspacePath); + if (!existsSync(filepath)) { + return { sessions: {} }; + } + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch (error) { + debugLog('Error reading psmm.json:', error.message); + return { sessions: {} }; + } +} + +function writePsmm(workspacePath, data) { + const filepath = getPsmmPath(workspacePath); + writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); +} + +function formatTimestamp() { + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_psmm_log", + description: "Log a per-session meta memory entry. Types: DECISION, CORRECTION, SHIFT, INSIGHT, COMMITMENT. Auto-creates session if new.", + inputSchema: { + type: "object", + properties: { + session_id: { type: "string", description: "Session UUID" }, + type: { type: "string", enum: VALID_TYPES, description: "Entry type" }, + text: { type: "string", description: "Description of the meta moment" } + }, + required: ["session_id", "type", "text"] + } + }, + { + name: "base_psmm_get", + description: "Get all PSMM entries for a specific session by UUID.", + inputSchema: { + type: "object", + properties: { + session_id: { type: "string", description: "Session UUID" } + }, + required: ["session_id"] + } + }, + { + name: "base_psmm_list", + description: "List all PSMM sessions with entry counts and created timestamps.", + inputSchema: { type: "object", properties: {} } + }, + { + name: "base_psmm_clean", + description: "Remove a stale session's entries from PSMM.", + inputSchema: { + type: "object", + properties: { + session_id: { type: "string", description: "Session UUID to remove" } + }, + required: ["session_id"] + } + } +]; + +// ============================================================ +// TOOL HANDLERS +// ============================================================ + +export async function handleTool(name, args, workspacePath) { + switch (name) { + case "base_psmm_log": return psmmLog(args, workspacePath); + case "base_psmm_get": return psmmGet(args, workspacePath); + case "base_psmm_list": return psmmList(workspacePath); + case "base_psmm_clean": return psmmClean(args, workspacePath); + default: return null; + } +} + +async function psmmLog(args, workspacePath) { + const { session_id, type, text } = args; + + if (!VALID_TYPES.includes(type)) { + return { success: false, error: `Invalid type: ${type}. Valid: ${VALID_TYPES.join(', ')}` }; + } + + debugLog('Logging PSMM entry:', session_id, type); + + const data = readPsmm(workspacePath); + + if (!data.sessions[session_id]) { + data.sessions[session_id] = { + created: formatTimestamp(), + entries: [] + }; + } + + const entry = { + timestamp: formatTimestamp(), + type, + text + }; + + data.sessions[session_id].entries.push(entry); + writePsmm(workspacePath, data); + + return { + success: true, + session_id, + entry_count: data.sessions[session_id].entries.length, + message: `Logged ${type} entry to session ${session_id.slice(0, 8)}...` + }; +} + +async function psmmGet(args, workspacePath) { + const { session_id } = args; + debugLog('Getting PSMM for session:', session_id); + + const data = readPsmm(workspacePath); + const session = data.sessions[session_id]; + + if (!session) { + return { entries: [], exists: false }; + } + + return { + exists: true, + session_id, + created: session.created, + entry_count: session.entries.length, + entries: session.entries + }; +} + +async function psmmList(workspacePath) { + debugLog('Listing PSMM sessions'); + + const data = readPsmm(workspacePath); + const sessions = []; + + for (const [id, session] of Object.entries(data.sessions)) { + sessions.push({ + session_id: id, + created: session.created, + entry_count: session.entries.length, + types: [...new Set(session.entries.map(e => e.type))] + }); + } + + sessions.sort((a, b) => b.created.localeCompare(a.created)); + + return { + success: true, + session_count: sessions.length, + total_entries: sessions.reduce((sum, s) => sum + s.entry_count, 0), + sessions + }; +} + +async function psmmClean(args, workspacePath) { + const { session_id } = args; + debugLog('Cleaning PSMM session:', session_id); + + const data = readPsmm(workspacePath); + + if (!data.sessions[session_id]) { + return { success: false, error: `Session not found: ${session_id}` }; + } + + const entryCount = data.sessions[session_id].entries.length; + delete data.sessions[session_id]; + writePsmm(workspacePath, data); + + return { + success: true, + session_id, + entries_removed: entryCount, + message: `Cleaned session ${session_id.slice(0, 8)}... (${entryCount} entries removed)` + }; +} diff --git a/mcp/tools/satellite.js b/mcp/tools/satellite.js new file mode 100644 index 0000000..293c590 --- /dev/null +++ b/mcp/tools/satellite.js @@ -0,0 +1,243 @@ +/** + * BASE Satellite Sync — Real-time PAUL project state sync + * Reads paul.json from a satellite, syncs to workspace.json + projects.json + * Called by PAUL at end of each loop phase (plan, apply, unify, handoff) + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, relative } from 'path'; +import { validateSurface } from './validate.js'; + +function debugLog(...args) { + console.error('[BASE:satellite]', new Date().toISOString(), ...args); +} + +// ============================================================ +// HELPERS +// ============================================================ + +function readJson(filepath) { + if (!existsSync(filepath)) return null; + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch (e) { + return null; + } +} + +function writeJson(filepath, data) { + writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); +} + +function formatTimestamp() { + return new Date().toISOString(); +} + +function buildPaulField(paulData, satelliteName, satellitePath) { + const phase = paulData.phase || {}; + const loop = paulData.loop || {}; + const handoff = paulData.handoff || {}; + const milestone = paulData.milestone || {}; + const timestamps = paulData.timestamps || {}; + + const completedPhases = phase.status === 'complete' + ? phase.number + : Math.max(0, (phase.number || 1) - 1); + + return { + is_paul_project: true, + satellite_name: satelliteName, + location: satellitePath + '/', + milestone: milestone.name || null, + phase: phase.name || null, + phase_name: phase.name || null, + loop_position: loop.position || 'IDLE', + last_update: timestamps.updated_at || formatTimestamp(), + handoff: handoff.present || false, + handoff_path: handoff.path || null, + completed_phases: completedPhases, + total_phases: phase.total || null, + last_plan_completed_at: paulData.last_plan_completed_at || null, + }; +} + +function findProjectByPath(items, satellitePath) { + const pathVariants = [ + satellitePath, + satellitePath + '/', + satellitePath.replace(/\/$/, ''), + ]; + return items.find(item => { + const loc = (item.location || '').replace(/\/$/, ''); + return pathVariants.some(v => v.replace(/\/$/, '') === loc); + }); +} + +function findProjectBySatelliteName(items, name) { + return items.find(item => + item.paul && item.paul.satellite_name === name + ); +} + +// ============================================================ +// SYNC LOGIC +// ============================================================ + +function syncSatellite(paulJsonPath, workspacePath) { + const paulData = readJson(paulJsonPath); + if (!paulData) throw new Error(`Cannot read paul.json at ${paulJsonPath}`); + + const name = paulData.name; + if (!name) throw new Error('paul.json has no name field'); + + // Derive paths + const projectDir = join(paulJsonPath, '..', '..'); + const satellitePath = relative(workspacePath, projectDir); + const phase = paulData.phase || {}; + const loop = paulData.loop || {}; + const handoff = paulData.handoff || {}; + const timestamps = paulData.timestamps || {}; + + const result = { satellite: name, workspace_synced: false, project_synced: false, project_created: false }; + + // --- Sync workspace.json --- + const manifestPath = join(workspacePath, '.base', 'workspace.json'); + const manifest = readJson(manifestPath); + if (manifest) { + if (!manifest.satellites) manifest.satellites = {}; + const sat = manifest.satellites[name]; + + if (sat) { + // Update existing satellite + sat.last_activity = timestamps.updated_at || formatTimestamp(); + sat.phase_name = phase.name; + sat.phase_number = phase.number; + sat.phase_status = phase.status; + sat.loop_position = loop.position; + sat.handoff = handoff.present || false; + sat.last_plan_completed_at = paulData.last_plan_completed_at; + result.workspace_synced = true; + } else { + // New satellite — register + manifest.satellites[name] = { + path: satellitePath, + engine: 'paul', + state: satellitePath + '/.paul/STATE.md', + registered: new Date().toISOString().split('T')[0], + groom_check: true, + last_activity: timestamps.updated_at || formatTimestamp(), + phase_name: phase.name, + phase_number: phase.number, + phase_status: phase.status, + loop_position: loop.position, + handoff: handoff.present || false, + last_plan_completed_at: paulData.last_plan_completed_at, + }; + result.workspace_synced = true; + } + + writeJson(manifestPath, manifest); + } + + // --- Sync projects.json --- + const projectsPath = join(workspacePath, '.base', 'data', 'projects.json'); + const projectsData = readJson(projectsPath); + if (projectsData) { + let project = findProjectBySatelliteName(projectsData.items, name) + || findProjectByPath(projectsData.items, satellitePath); + + const paulField = buildPaulField(paulData, name, satellitePath); + + if (project) { + // Update existing — merge paul field, preserve user-set fields + if (!project.paul) project.paul = {}; + Object.assign(project.paul, paulField); + project.updated_at = formatTimestamp(); + result.project_synced = true; + } else { + // Auto-create project entry + const maxNum = projectsData.items + .filter(i => (i.id || '').startsWith('PRJ-')) + .reduce((max, i) => { + const n = parseInt((i.id || '').replace('PRJ-', ''), 10); + return n > max ? n : max; + }, 0); + + const newId = `PRJ-${String(maxNum + 1).padStart(3, '0')}`; + const title = paulData.project?.title || name; + const now = formatTimestamp(); + + projectsData.items.push({ + id: newId, + title, + type: 'project', + parent_id: null, + status: 'in_progress', + priority: 'medium', + category: 'internal', + assignees: [], + start_date: null, + due_date: null, + created_at: now, + updated_at: now, + location: satellitePath + '/', + blocked_by: null, + next: null, + notes: [], + tags: [], + paul: paulField, + relations: [], + description: null, + }); + result.project_created = true; + result.project_id = newId; + } + + projectsData.last_modified = formatTimestamp(); + validateSurface('projects', projectsData); + writeJson(projectsPath, projectsData); + } + + debugLog(`Synced satellite: ${name} (ws:${result.workspace_synced}, prj:${result.project_synced}, new:${result.project_created})`); + return result; +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_sync_satellite", + description: "Sync a PAUL satellite's state to workspace.json and projects.json. Reads paul.json, updates satellite entry and matching project. Creates project entry if none exists. Call after plan/apply/unify/handoff.", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Workspace-relative path to the PAUL project (e.g., 'apps/my-app')" }, + }, + required: ["path"] + } + } +]; + +// ============================================================ +// HANDLER DISPATCH +// ============================================================ + +export function handleTool(name, args, workspacePath) { + switch (name) { + case 'base_sync_satellite': { + const { path: projectPath } = args; + if (!projectPath) throw new Error('Missing required parameter: path'); + + const paulJsonPath = join(workspacePath, projectPath, '.paul', 'paul.json'); + if (!existsSync(paulJsonPath)) { + throw new Error(`No paul.json found at ${projectPath}/.paul/paul.json`); + } + + return syncSatellite(paulJsonPath, workspacePath); + } + default: + return null; + } +} diff --git a/mcp/tools/state.js b/mcp/tools/state.js new file mode 100644 index 0000000..04fc7cd --- /dev/null +++ b/mcp/tools/state.js @@ -0,0 +1,201 @@ +/** + * BASE State — Read/update tools for state.json + * Workspace health, drift tracking, groom scheduling + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { validateSurface } from './validate.js'; + +function debugLog(...args) { + console.error('[BASE:state]', new Date().toISOString(), ...args); +} + +// ============================================================ +// HELPERS +// ============================================================ + +function getStatePath(workspacePath) { + return join(workspacePath, '.base', 'data', 'state.json'); +} + +function readState(workspacePath) { + const filepath = getStatePath(workspacePath); + if (!existsSync(filepath)) { + return { version: 1, workspace: '', last_modified: null, groom: {}, drift: { score: 0, indicators: {} }, areas: {}, satellites: {} }; + } + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch (error) { + debugLog('Error reading state.json:', error.message); + return { version: 1, workspace: '', last_modified: null, groom: {}, drift: { score: 0, indicators: {} }, areas: {}, satellites: {} }; + } +} + +function writeState(workspacePath, data) { + const filepath = getStatePath(workspacePath); + data.last_modified = new Date().toISOString(); + validateSurface('state', data); + writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); +} + +function addDays(dateStr, days) { + const d = new Date(dateStr); + d.setDate(d.getDate() + days); + return d.toISOString().split('T')[0]; +} + +function todayStr() { + return new Date().toISOString().split('T')[0]; +} + +// ============================================================ +// TOOL DEFINITIONS +// ============================================================ + +export const TOOLS = [ + { + name: "base_get_state", + description: "Read full workspace state (groom, drift, areas, satellites, carl_hygiene). Returns entire state.json.", + inputSchema: { + type: "object", + properties: {}, + required: [] + } + }, + { + name: "base_update_drift", + description: "Update drift indicators and recalculate composite score. Pass indicator key-value pairs to merge.", + inputSchema: { + type: "object", + properties: { + indicators: { + type: "object", + description: "Drift indicator updates (e.g., { active_age_days: 2, backlog_past_review: 3 })" + } + }, + required: ["indicators"] + } + }, + { + name: "base_record_groom", + description: "Record a groom event. Sets last_groom to today and advances next_groom_due based on cadence.", + inputSchema: { + type: "object", + properties: {}, + required: [] + } + }, + { + name: "base_update_area", + description: "Update a specific workspace area's fields (status, last_touched, groom_due, etc.).", + inputSchema: { + type: "object", + properties: { + area: { type: "string", description: "Area slug (key in state.json areas object)" }, + data: { type: "object", description: "Fields to merge into the area object" } + }, + required: ["area", "data"] + } + } +]; + +// ============================================================ +// TOOL HANDLERS +// ============================================================ + +function handleGetState(workspacePath) { + debugLog('Reading state'); + return readState(workspacePath); +} + +function handleUpdateDrift(args, workspacePath) { + const { indicators } = args; + if (!indicators) throw new Error('Missing required parameter: indicators'); + + debugLog('Updating drift indicators'); + const data = readState(workspacePath); + + if (!data.drift) data.drift = { score: 0, indicators: {} }; + if (!data.drift.indicators) data.drift.indicators = {}; + + // Merge indicators + data.drift.indicators = { ...data.drift.indicators, ...indicators }; + + // Recalculate score as sum of all indicator values + data.drift.score = Object.values(data.drift.indicators) + .reduce((sum, val) => sum + (typeof val === 'number' ? val : 0), 0); + + writeState(workspacePath, data); + + return { + score: data.drift.score, + indicators: data.drift.indicators + }; +} + +function handleRecordGroom(workspacePath) { + debugLog('Recording groom event'); + const data = readState(workspacePath); + + if (!data.groom) data.groom = { cadence: 'weekly', day: 'friday' }; + + const today = todayStr(); + data.groom.last_groom = today; + + // Calculate next due based on cadence + const cadenceDays = { + daily: 1, + weekly: 7, + 'bi-weekly': 14, + monthly: 30 + }; + const days = cadenceDays[data.groom.cadence] || 7; + data.groom.next_groom_due = addDays(today, days); + + writeState(workspacePath, data); + + return { + last_groom: data.groom.last_groom, + next_groom_due: data.groom.next_groom_due, + cadence: data.groom.cadence + }; +} + +function handleUpdateArea(args, workspacePath) { + const { area, data: updateData } = args; + if (!area) throw new Error('Missing required parameter: area'); + if (!updateData) throw new Error('Missing required parameter: data'); + + debugLog('Updating area:', area); + const data = readState(workspacePath); + + if (!data.areas) data.areas = {}; + if (!data.areas[area]) { + throw new Error(`Area "${area}" not found. Available: ${Object.keys(data.areas).join(', ') || 'none'}`); + } + + data.areas[area] = { ...data.areas[area], ...updateData }; + writeState(workspacePath, data); + + return data.areas[area]; +} + +// ============================================================ +// HANDLER DISPATCH +// ============================================================ + +export function handleTool(name, args, workspacePath) { + switch (name) { + case 'base_get_state': + return handleGetState(workspacePath); + case 'base_update_drift': + return handleUpdateDrift(args, workspacePath); + case 'base_record_groom': + return handleRecordGroom(workspacePath); + case 'base_update_area': + return handleUpdateArea(args, workspacePath); + default: + return null; + } +} diff --git a/mcp/tools/validate.js b/mcp/tools/validate.js new file mode 100644 index 0000000..603e003 --- /dev/null +++ b/mcp/tools/validate.js @@ -0,0 +1,121 @@ +/** + * Lightweight schema validation for BASE data surfaces. + * No external dependencies — enforces required fields and basic structure + * based on schemas/*.schema.json definitions. + * + * Validates on write to catch data corruption early. + * Logs warnings rather than throwing — defensive, won't block operations. + */ + +function debugLog(...args) { + console.error('[BASE:validate]', new Date().toISOString(), ...args); +} + +// ============================================================ +// SCHEMA DEFINITIONS (derived from schemas/*.schema.json) +// ============================================================ + +const SCHEMAS = { + projects: { + requiredRoot: ['items'], + itemFields: ['id', 'title', 'type', 'status', 'priority', 'created_at', 'updated_at'], + validTypes: ['initiative', 'project', 'task'], + validStatuses: ['backlog', 'todo', 'in_progress', 'blocked', 'in_review', 'completed', 'deferred', 'archived'], + validPriorities: ['urgent', 'high', 'medium', 'low', 'ongoing'], + idPattern: /^(INI|PRJ|TSK)-\d{3,}$/ + }, + entities: { + requiredRoot: ['entities'], + itemFields: ['id', 'name', 'type', 'created_at', 'updated_at'], + validTypes: ['person', 'organization'], + idPattern: /^ENT-\d{3,}$/ + }, + state: { + requiredRoot: ['groom', 'drift', 'areas'], + requiredGroom: ['cadence'], + validCadences: ['daily', 'weekly', 'bi-weekly', 'monthly'], + validAreaStatuses: ['current', 'stale', 'critical'] + } +}; + +// ============================================================ +// VALIDATORS +// ============================================================ + +/** + * Validate a data surface before writing. + * Returns { valid: boolean, warnings: string[] } + */ +export function validateSurface(surfaceName, data) { + const schema = SCHEMAS[surfaceName]; + if (!schema) { + return { valid: true, warnings: [] }; + } + + const warnings = []; + + // Check required root fields + if (schema.requiredRoot) { + for (const field of schema.requiredRoot) { + if (data[field] === undefined) { + warnings.push(`Missing required root field: ${field}`); + } + } + } + + // Validate items array (projects, entities) + if (schema.itemFields && Array.isArray(data.items || data.entities)) { + const items = data.items || data.entities; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + for (const field of schema.itemFields) { + if (item[field] === undefined) { + warnings.push(`Item ${item.id || `[${i}]`}: missing required field '${field}'`); + } + } + + // Validate ID pattern + if (schema.idPattern && item.id && !schema.idPattern.test(item.id)) { + warnings.push(`Item ${item.id}: ID does not match pattern ${schema.idPattern}`); + } + + // Validate enum fields + if (schema.validTypes && item.type && !schema.validTypes.includes(item.type)) { + warnings.push(`Item ${item.id || `[${i}]`}: invalid type '${item.type}'`); + } + if (schema.validStatuses && item.status && !schema.validStatuses.includes(item.status)) { + warnings.push(`Item ${item.id || `[${i}]`}: invalid status '${item.status}'`); + } + if (schema.validPriorities && item.priority && !schema.validPriorities.includes(item.priority)) { + warnings.push(`Item ${item.id || `[${i}]`}: invalid priority '${item.priority}'`); + } + } + } + + // State-specific validation + if (surfaceName === 'state') { + if (data.groom && schema.requiredGroom) { + for (const field of schema.requiredGroom) { + if (data.groom[field] === undefined) { + warnings.push(`groom: missing required field '${field}'`); + } + } + if (data.groom.cadence && !schema.validCadences.includes(data.groom.cadence)) { + warnings.push(`groom: invalid cadence '${data.groom.cadence}'`); + } + } + if (data.areas) { + for (const [areaName, area] of Object.entries(data.areas)) { + if (area.status && !schema.validAreaStatuses.includes(area.status)) { + warnings.push(`area '${areaName}': invalid status '${area.status}'`); + } + } + } + } + + if (warnings.length > 0) { + debugLog(`Validation warnings for ${surfaceName}:`, warnings); + } + + return { valid: warnings.length === 0, warnings }; +} diff --git a/skills/base/base.md b/skills/base/base.md new file mode 100644 index 0000000..fecb427 --- /dev/null +++ b/skills/base/base.md @@ -0,0 +1,110 @@ +--- +name: base +type: suite +version: 0.1.0 +category: workspace-orchestration +description: "Builder's Automated State Engine — workspace lifecycle management for Claude Code. Scaffold, audit, groom, and maintain AI builder workspaces. Manage data surfaces for structured context injection. Use when user mentions workspace setup, cleanup, organization, maintenance, grooming, auditing workspace health, surfaces, or BASE." +allowed-tools: [Read, Write, Glob, Grep, Edit, Bash, Agent, AskUserQuestion] +--- + + + +## What +BASE (Builder's Automated State Engine) manages the lifecycle of a Claude Code workspace. It scaffolds new workspaces, audits existing ones, runs structured grooming cycles, and maintains workspace health through automated drift detection. + +## When to Use +- User says "base", "workspace", "cleanup", "organize", "audit my workspace", "groom", "surface", "create a surface" +- User wants to set up a new workspace from scratch +- User wants to optimize or clean up an existing workspace +- User asks about workspace health, staleness, or drift +- Session start hook detects overdue grooming +- User wants to review workspace evolution history + +## Not For +- Project-level build orchestration (that's PAUL) +- Session-level rule management (that's CARL) +- Code quality auditing (that's AEGIS) +- Skill/tool creation (that's Skillsmith) + + + + + +## Role +Workspace operations engineer. Knows the territory, tracks what's drifting, enforces maintenance cadence. Tactical, not theoretical. + +## Style +- Direct, structured, checklist-driven +- Presents health dashboards and drift scores +- Asks focused questions during grooming (voice-friendly) +- Never skips areas — systematic coverage +- Recommends, doesn't dictate + +## Expertise +- Workspace architecture and file organization +- Context document lifecycle (projects.json, state.json, entities.json) +- Tool and configuration management +- Drift detection and prevention patterns +- Claude Code ecosystem (PAUL, CARL, AEGIS, Skillsmith integration) + + + + + +| Command | Description | Routes To | +|---------|------------|-----------| +| `/base:pulse` | Daily activation — workspace health briefing | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/pulse.md` | +| `/base:groom` | Weekly maintenance cycle | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/groom.md` | +| `/base:audit` | Deep workspace optimization | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit.md` | +| `/base:scaffold` | Set up BASE in a new workspace | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/scaffold.md` | +| `/base:status` | Quick health check (one-liner) | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/status.md` | +| `/base:history` | Workspace evolution timeline | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/history.md` | +| `/base:audit-claude-md` | Audit CLAUDE.md, generate recommended version | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit-claude-md.md` | +| `/base:carl-hygiene` | CARL domain maintenance and rule review | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/carl-hygiene.md` | +| `/base:surface create` | Create a new data surface (guided) | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-create.md` | +| `/base:surface convert` | Convert markdown file to data surface | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-convert.md` | +| `/base:surface list` | Show all registered surfaces | `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-list.md` | + + + + + +## Always Load +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/context/base-principles.md` — Core workspace management principles +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/audit-strategies.md` — Reusable audit strategy definitions + +## Load on Command +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/pulse.md` — on `/base:pulse` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/groom.md` — on `/base:groom` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/audit.md` — on `/base:audit` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/scaffold.md` — on `/base:scaffold` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/status.md` — on `/base:status` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/history.md` — on `/base:history` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/carl-hygiene.md` — on `/base:carl-hygiene` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-create.md` — on `/base:surface create` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-convert.md` — on `/base:surface convert` +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/tasks/surface-list.md` — on `/base:surface list` + +## Load on Demand +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/templates/workspace-json.md` — When generating workspace.json +- `@${CLAUDE_PLUGIN_ROOT}/base-framework/frameworks/satellite-registration.md` — When handling PAUL project registration + + + + + +BASE loaded. Builder's Automated State Engine. + +Available commands: +- `/base:pulse` — What's the state of my workspace? +- `/base:groom` — Run weekly maintenance +- `/base:audit` — Deep optimization session +- `/base:scaffold` — Set up BASE in a new workspace +- `/base:status` — Quick health check +- `/base:history` — Workspace evolution timeline +- `/base:surface create` — Create a new data surface +- `/base:surface list` — Show registered surfaces + +What do you need? + + From 3fb2eab3d3fd723d7131c2edb9f887aba4f145ea Mon Sep 17 00:00:00 2001 From: Madison Steiner Date: Sat, 13 Jun 2026 23:14:02 -0700 Subject: [PATCH 2/6] chore: remove __pycache__ from hooks/ and add to .gitignore Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + hooks/__pycache__/active-hook.cpython-314.pyc | Bin 10313 -> 0 bytes hooks/__pycache__/apex-insights.cpython-314.pyc | Bin 11087 -> 0 bytes hooks/__pycache__/backlog-hook.cpython-314.pyc | Bin 5636 -> 0 bytes .../base-pulse-check.cpython-314.pyc | Bin 10538 -> 0 bytes hooks/__pycache__/operator.cpython-314.pyc | Bin 3189 -> 0 bytes hooks/__pycache__/psmm-injector.cpython-314.pyc | Bin 3662 -> 0 bytes .../satellite-detection.cpython-314.pyc | Bin 15549 -> 0 bytes 8 files changed, 1 insertion(+) delete mode 100644 hooks/__pycache__/active-hook.cpython-314.pyc delete mode 100644 hooks/__pycache__/apex-insights.cpython-314.pyc delete mode 100644 hooks/__pycache__/backlog-hook.cpython-314.pyc delete mode 100644 hooks/__pycache__/base-pulse-check.cpython-314.pyc delete mode 100644 hooks/__pycache__/operator.cpython-314.pyc delete mode 100644 hooks/__pycache__/psmm-injector.cpython-314.pyc delete mode 100644 hooks/__pycache__/satellite-detection.cpython-314.pyc diff --git a/.gitignore b/.gitignore index de9dc4a..724a137 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ DIRECTORY-STRATEGY-SPEC.md # Node node_modules/ +__pycache__/ diff --git a/hooks/__pycache__/active-hook.cpython-314.pyc b/hooks/__pycache__/active-hook.cpython-314.pyc deleted file mode 100644 index 944c4d1827184081f8642fc4c703a0b1030780b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10313 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

ZD&0XwkAo)JY*{-r2Oj6pU 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 IQ4aP013W+6LjV8( diff --git a/hooks/__pycache__/backlog-hook.cpython-314.pyc b/hooks/__pycache__/backlog-hook.cpython-314.pyc deleted file mode 100644 index 02e7fe1f82b71b0edc979bb32017b4714679a7d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5636 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 E0(~T`C;$Ke diff --git a/hooks/__pycache__/base-pulse-check.cpython-314.pyc b/hooks/__pycache__/base-pulse-check.cpython-314.pyc deleted file mode 100644 index 0a6b8f48b3c1d7c60d4e211fd12e1737dd9e7140..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10538 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 diff --git a/hooks/__pycache__/operator.cpython-314.pyc b/hooks/__pycache__/operator.cpython-314.pyc deleted file mode 100644 index f10311379b81d290558987091d2b2e7f29c4df6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3189 zcmbVOT}&I<6}~eb+wC6~{ z_o42~3bqdr^&vb|6%SR#J~c0uc&t=u)khp>cbvOhZL2=?mEELjAF8Tn3z~^NVw119AA6;-G^f#+xH|j`u{T~9MG7^x1eu4yIk}wG1*5x>Kf=+Ex{e~Mtbe2lkhZ-9?FKhZj&~cI%f?o#U$EK>m|BQj|mRjtKm!e zt{f?3qX>1QlrPbHdWDVTSQk8jt`&jLw)MXvFap!_Dfiprc-xo}NCgCHuwh3M7thD^ z5Oe?&mw4y2Zo&rMFeNxsfkD^FfE@>%%y+yCu0||Ii{NfUMkv)P&^PWQl!Z(Hzo9;4 z?@}+E_Qm=aW=g+j?~Kfi^An2lc$imINlP1w7FE~SIi;v&q+ve#AgxQW6|Ww*_kkbsYKdX#!aIij?9mZ ziwR+NYJ4;)j!gQt#S{Cm&w?`w#`b%qW9IW__fHrz?4ea_jYdbR}CZOlKuY6<3$nFaz3#WYsXtK9<(R zm2|Es>DYC|opp|VdQn?SXC!e&)@7xD-A7dr`=p$-VwF}=m5t0YcGg*m9OO$R-;arK zsqLhgvNIMweh%;r^CgZQ0}%YeGStZu!``bqJ{0RtK&07(Wfde3d+WGX-wv~w85>EC zi1En}$FWO#BI|~ZDXUOnXHH3Hbyg#-7uA;c9TKJXH&{Jfok|+ub`_H4rfa(Mlk?`_bN= zYT$?E$r?C%vhie_-MLfsUorgy75~8Q-Mx;gf3!SNi$s6d|7rj8TYqS(M#jv@L?tru zr}q81`InJFw<25J+rkdB{n2h;#Xq=LDo-3xM3cAX4XsZdAfk<{an0)ykU~t2XYwe0(cYJn@0E%f*w9Zu&!)@3tidiKiiW(G7gxxA! z_Br{1W?fL0-jbY?L=n3+Nmp_!5_YL+&H6d%iP_n^)++#$3jChpgJlu8LtoRuB6RA4 zR9KNUrJz}l&qEzGi=B|5vWk5Rv%=lE#K`ElD9p|#u}jv)tPF)hH?oqZVdttQ8w$;qTX`j0%t^Pko6xmh(fSuaU@HjX8_G#guYHK{{28_X zmGf`WzqoC3eHE^+%3U~qh1W#(HKPc$9G a2}F6;OD`QAuPFNC_%{wjb!x*9+J6B9*{Rw9 diff --git a/hooks/__pycache__/psmm-injector.cpython-314.pyc b/hooks/__pycache__/psmm-injector.cpython-314.pyc deleted file mode 100644 index c23c4737edfbbc8bd834746c43345ab64121cf23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3662 zcmb7GT~Hg>6~3!oNvmD`fdv>a;es5_NhEY*l||^UWMVaXckt*t3ZXm_kb$ltff}IO)P$9E@DfBovt*l@p-e5jwDAvB&x@Fo*b>ykP&2nWKJYcrRTc-U?1j_P=@iB6_F1 zQLyakX2s32es+JwdhSIu3v3HA{8J5r>%74_5xYT;`w+A#PYv zZVq5oPo;u#`i3-PC|XF(a?y;YD!Md)qmmXJmvmiL()f~Oh(J<`mc@U`{~YfKU`d^m zQj#VnvA)ZwOzV<1FB%}EbJ%Kf+EB1GFM+hGDJj*!x`K^4DT{SO%;Gu40F5}Qq-QNf zI)3%YCA=V~6Ustwh^wiZfSs_2bvZqolq%MTZs=g)bs44$vN30A2;P*kQUXh9Lz5*v zRMQX6B8rn>U518B5b?+5$w_= zJUVt6iy1=+&Lkx<4Hl4i8FQl1qZ600qC&tR)-b)HOxMhsmqk2yR=9}gva_<3mT(xq zadtd{6PkS8zzNBK6N0EhEG}#E?5w2e0~ms#MJoXv&rGLegB!~jYQ`AAgBeW&&n?Sf zuju%tOI9$jsAD~=8&V3-C}~5wWgNna1~E+%8TmSv;E1vzZnT#&nW$*YnT>-N&rY0+ z#G}I4n~}lG@pB_W&K{Z;b;)EBq9Nv7B#$a5VN#))`UH2wgdonN>kL7gb~IW28ih1` zx^2nDix!w`<7@Yw`H=5TG4%$ zM6I{>uw-UiDl#=&12tZ2)CIU|+0^;}cwVyrv_IV}3oMPQ?TT$rFKuB|^^%U!d-24m zsum{3SUx2xxD#XclBnUV#r@(Dh%o|vsIBIgY7ZgR5wrWz`Ei6)XUw_h&WvEM`olhl z$5D5(uIKPL>v*`BW6x^B#yB?;Trn0X7jwDM(Q3u(L>DFya-o+{%wfeK0V@a4b0WqI z?yBAVB;?pmi`lEOcTaX$tAZy+3EukXfCavo8>m0V18tbBR*q|&r6Jm7$=CW=)nbrp zJnFJ|JL_-O|IAil-$8`y?<%>+Pqj&}wQ$VlKnW*_1*tPC)qx_^#b3$jB;A2z>IL;w zZsUttTiLH*?bjPHxm`?-%Lx;itcB2~bGIItjFD9Vya25uFa`)TU51>JpdO{v938|z z%GsuH&NhHe=Zq$ahLkX!ne@#x)QOz$6hS)+Tu*lojQ$XPC1FB$ zpW$*WcPdOI&y4mqm<%afrah^M3EiXtj!nC6B;>Toz;+3fhRS66-W(qrJtxgT1&U~z zqM6RI@tuK-;CcM~$i;~1kZ#Gkp_>f!;02M6R~&MPsw$;H6~MY_S2d`@COtE!!PbhL z?)8|QoQ7%BhN^%Qcg=?APHX9{4x;}G#>HW-G#D|MbM+#iIsdMGp?Q3fn*Sm{NuNAl_EOIx0 za<$YE+~^3FI>HY+!dooA#D2&wH~f^}W@vk0%eQYS_hIgn-cnm|qb*o$3m1Kd3(Pi) z{DGxgi?>#}wKs~sp8Rmx+q{%qOs)C@1_I%p^i~cpa)OPqm+u?e>4&-ZI zmN&gPKU8*m^O0{F_808srnZ%mLSU;eu#{cQt~ReV7JV-lm~xdiYxi-AzVRc1X)?nU>qzWVxV|C(@zTl?|)(e=c7?0(a!Kd`4Cx=>^56E|W#<;K>P z{@eYl@}1UV5_uS|E9wW-WzueUF5w}gl=wFZh&wdY71GB^vuE0O_m*F_yJIf5Upx^x9)uEH< ztCo(T6ZBW#rwBcHWcVIz)2yAJMKuo6!?~9z8o4i3?+6mkAck%bJqb zNU=5Dad?`NXK*|rYZ|Fi8iDLe9uoa&F$Ma6i5|fs(t3fI?5UKJ$RwpR+UqcdG)4OF zfPl9Y^^kE>%%cN{;=V!fWL@qHV7U!E;Dw61jC?kqL* zY&7)by?^#KEaet+OYbbcbN{8IMc=W!`%kp*Yky#Qvgq$#du!8w=+6A6|FygPrho9= zk#FI{_{Xl2ziY$aRrDWRo89yW*BzVw@7-TrjaOmH|{`$=T diff --git a/hooks/__pycache__/satellite-detection.cpython-314.pyc b/hooks/__pycache__/satellite-detection.cpython-314.pyc deleted file mode 100644 index 9c3c60cb10006f8fc32930b15c2a258f8e56dde4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15549 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 From 8c3c3c3433d8cac86b37356515aeff4420c2ff3e Mon Sep 17 00:00:00 2001 From: Madison Steiner <8176115+mh0pe@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:13:48 -0700 Subject: [PATCH 3/6] refactor(plugin): converge to single plugin-native source tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the duplicate src/ framework/command/hook/mcp trees that shadowed the root plugin-native tree (commands/, base-framework/, skills/, hooks/, mcp/). The root tree uses ${CLAUDE_PLUGIN_ROOT} macros and is the single committed source of truth. bin/install.js now reads from the root tree for all install modes (--global, --local, --workspace). copyFileExpandingMacro is now exercised for every copy path, substituting ${CLAUDE_PLUGIN_ROOT} with the resolved install target so no literal placeholder survives in npx-installed output. Retained: src/templates/ (workspace.json + operator.json) — these are the npx workspace-mode templates; they contain no macros and have no equivalent in the root tree. package.json files[] updated to ship the root tree instead of src/. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/install.js | 55 +- package.json | 10 +- src/commands/audit-claude-md.md | 44 -- src/commands/audit-claude.md | 45 -- src/commands/audit.md | 33 -- src/commands/carl-hygiene.md | 33 -- src/commands/groom.md | 35 -- src/commands/history.md | 27 - src/commands/orientation.md | 87 --- src/commands/orientation/tasks/deep-why.md | 132 ----- .../orientation/tasks/elevator-pitch.md | 115 ---- src/commands/orientation/tasks/initiatives.md | 98 ---- src/commands/orientation/tasks/key-values.md | 130 ----- .../orientation/tasks/new-orientation.md | 162 ------ src/commands/orientation/tasks/north-star.md | 97 ---- .../orientation/tasks/project-mapping.md | 103 ---- .../orientation/tasks/reorientation.md | 96 --- .../orientation/tasks/surface-vision.md | 113 ---- .../orientation/tasks/task-seeding.md | 93 --- .../orientation/templates/operator-json.md | 88 --- src/commands/pulse.md | 33 -- src/commands/scaffold.md | 33 -- src/commands/status.md | 28 - src/commands/surface-convert.md | 35 -- src/commands/surface-create.md | 34 -- src/commands/surface-list.md | 27 - src/commands/weekly-domain.md | 34 -- src/commands/weekly.md | 39 -- src/framework/context/base-principles.md | 69 --- src/framework/frameworks/audit-strategies.md | 53 -- .../frameworks/claude-config-alignment.md | 256 -------- src/framework/frameworks/claudemd-strategy.md | 158 ----- .../frameworks/satellite-registration.md | 44 -- src/framework/tasks/audit-claude-md.md | 171 ------ src/framework/tasks/audit-claude.md | 330 ----------- src/framework/tasks/audit.md | 64 -- src/framework/tasks/carl-hygiene.md | 142 ----- src/framework/tasks/groom.md | 157 ----- src/framework/tasks/history.md | 34 -- src/framework/tasks/pulse.md | 83 --- src/framework/tasks/scaffold.md | 389 ------------- src/framework/tasks/status.md | 35 -- src/framework/tasks/surface-convert.md | 143 ----- src/framework/tasks/surface-create.md | 184 ------ src/framework/tasks/surface-list.md | 42 -- src/framework/tasks/weekly-domain-create.md | 173 ------ src/framework/tasks/weekly.md | 347 ----------- src/framework/templates/claudemd-template.md | 102 ---- src/framework/templates/workspace-json.md | 96 --- src/framework/utils/scan-claude-dirs.py | 549 ------------------ src/hooks/_template.py | 130 ----- src/hooks/active-hook.py | 178 ------ src/hooks/apex-insights.py | 169 ------ src/hooks/backlog-hook.py | 115 ---- src/hooks/base-pulse-check.py | 216 ------- src/hooks/operator.py | 53 -- src/hooks/psmm-injector.py | 67 --- src/hooks/satellite-detection.py | 320 ---------- src/packages/base-mcp/index.js | 119 ---- src/packages/base-mcp/package.json | 10 - src/packages/base-mcp/tools/entities.js | 228 -------- src/packages/base-mcp/tools/operator.js | 106 ---- src/packages/base-mcp/tools/projects.js | 324 ----------- src/packages/base-mcp/tools/psmm.js | 206 ------- src/packages/base-mcp/tools/satellite.js | 243 -------- src/packages/base-mcp/tools/state.js | 201 ------- src/packages/base-mcp/tools/validate.js | 121 ---- src/skill/base.md | 110 ---- 68 files changed, 35 insertions(+), 8361 deletions(-) delete mode 100644 src/commands/audit-claude-md.md delete mode 100644 src/commands/audit-claude.md delete mode 100644 src/commands/audit.md delete mode 100644 src/commands/carl-hygiene.md delete mode 100644 src/commands/groom.md delete mode 100644 src/commands/history.md delete mode 100644 src/commands/orientation.md delete mode 100644 src/commands/orientation/tasks/deep-why.md delete mode 100644 src/commands/orientation/tasks/elevator-pitch.md delete mode 100644 src/commands/orientation/tasks/initiatives.md delete mode 100644 src/commands/orientation/tasks/key-values.md delete mode 100644 src/commands/orientation/tasks/new-orientation.md delete mode 100644 src/commands/orientation/tasks/north-star.md delete mode 100644 src/commands/orientation/tasks/project-mapping.md delete mode 100644 src/commands/orientation/tasks/reorientation.md delete mode 100644 src/commands/orientation/tasks/surface-vision.md delete mode 100644 src/commands/orientation/tasks/task-seeding.md delete mode 100644 src/commands/orientation/templates/operator-json.md delete mode 100644 src/commands/pulse.md delete mode 100644 src/commands/scaffold.md delete mode 100644 src/commands/status.md delete mode 100644 src/commands/surface-convert.md delete mode 100644 src/commands/surface-create.md delete mode 100644 src/commands/surface-list.md delete mode 100644 src/commands/weekly-domain.md delete mode 100644 src/commands/weekly.md delete mode 100644 src/framework/context/base-principles.md delete mode 100644 src/framework/frameworks/audit-strategies.md delete mode 100644 src/framework/frameworks/claude-config-alignment.md delete mode 100644 src/framework/frameworks/claudemd-strategy.md delete mode 100644 src/framework/frameworks/satellite-registration.md delete mode 100644 src/framework/tasks/audit-claude-md.md delete mode 100644 src/framework/tasks/audit-claude.md delete mode 100644 src/framework/tasks/audit.md delete mode 100644 src/framework/tasks/carl-hygiene.md delete mode 100644 src/framework/tasks/groom.md delete mode 100644 src/framework/tasks/history.md delete mode 100644 src/framework/tasks/pulse.md delete mode 100644 src/framework/tasks/scaffold.md delete mode 100644 src/framework/tasks/status.md delete mode 100644 src/framework/tasks/surface-convert.md delete mode 100644 src/framework/tasks/surface-create.md delete mode 100644 src/framework/tasks/surface-list.md delete mode 100644 src/framework/tasks/weekly-domain-create.md delete mode 100644 src/framework/tasks/weekly.md delete mode 100644 src/framework/templates/claudemd-template.md delete mode 100644 src/framework/templates/workspace-json.md delete mode 100644 src/framework/utils/scan-claude-dirs.py delete mode 100644 src/hooks/_template.py delete mode 100644 src/hooks/active-hook.py delete mode 100644 src/hooks/apex-insights.py delete mode 100644 src/hooks/backlog-hook.py delete mode 100644 src/hooks/base-pulse-check.py delete mode 100644 src/hooks/operator.py delete mode 100644 src/hooks/psmm-injector.py delete mode 100644 src/hooks/satellite-detection.py delete mode 100644 src/packages/base-mcp/index.js delete mode 100644 src/packages/base-mcp/package.json delete mode 100644 src/packages/base-mcp/tools/entities.js delete mode 100644 src/packages/base-mcp/tools/operator.js delete mode 100644 src/packages/base-mcp/tools/projects.js delete mode 100644 src/packages/base-mcp/tools/psmm.js delete mode 100644 src/packages/base-mcp/tools/satellite.js delete mode 100644 src/packages/base-mcp/tools/state.js delete mode 100644 src/packages/base-mcp/tools/validate.js delete mode 100644 src/skill/base.md diff --git a/bin/install.js b/bin/install.js index dbf77d2..3c209c4 100644 --- a/bin/install.js +++ b/bin/install.js @@ -385,43 +385,42 @@ function installCommands(isGlobal) { console.log(` Installing commands to ${cyan}${locationLabel}${reset}\n`); - // Copy commands - const commandsSrc = path.join(src, 'src', 'commands'); + // Copy commands (root tree, ${CLAUDE_PLUGIN_ROOT} → resolved claudeDir) + const commandsSrc = path.join(src, 'commands'); const commandsDest = path.join(claudeDir, 'commands', 'base'); - copyDir(commandsSrc, commandsDest); + copyDir(commandsSrc, commandsDest, claudeDir); const commandCount = fs.readdirSync(commandsSrc).filter(f => f.endsWith('.md')).length; console.log(` ${green}+${reset} commands/base/ (${commandCount} slash commands)`); - // Copy skill entry point - const skillSrc = path.join(src, 'src', 'skill'); + // Copy skill entry point (root tree) + const skillSrc = path.join(src, 'skills', 'base'); const skillDest = path.join(claudeDir, 'skills', 'base'); - copyDir(skillSrc, skillDest); - console.log(` ${green}+${reset} skills/base/ (entry point + MCP package sources)`); - - // Copy MCP package sources into skill (for scaffold reference) - const packagesSrc = path.join(src, 'src', 'packages'); - const packagesDest = path.join(claudeDir, 'skills', 'base', 'packages'); - copyDir(packagesSrc, packagesDest); + copyDir(skillSrc, skillDest, claudeDir); + console.log(` ${green}+${reset} skills/base/ (entry point)`); // Copy MCP package to base-framework/packages/ (global source for scaffold) const frameworkPackagesDest = path.join(claudeDir, 'base-framework', 'packages', 'base-mcp'); fs.mkdirSync(frameworkPackagesDest, { recursive: true }); - copyDir(path.join(src, 'src', 'packages', 'base-mcp'), frameworkPackagesDest); + copyDir(path.join(src, 'mcp'), frameworkPackagesDest, claudeDir); console.log(` ${green}+${reset} base-framework/packages/base-mcp/ (global MCP source for scaffold)`); - // Copy BASE framework (tasks, templates, context, frameworks) - const frameworkSrc = path.join(src, 'src', 'framework'); + // Copy BASE framework (tasks, templates, context, frameworks) from root tree + const frameworkSrc = path.join(src, 'base-framework'); const frameworkDest = path.join(claudeDir, 'base-framework'); - copyDir(frameworkSrc, frameworkDest); + copyDir(frameworkSrc, frameworkDest, claudeDir); console.log(` ${green}+${reset} base-framework/ (tasks, templates, context, frameworks, utils)`); - // Copy all hooks to base-framework/hooks/ (source for scaffold) + // Copy all hooks to base-framework/hooks/ (source for scaffold), from root tree const hooksFrameworkDest = path.join(claudeDir, 'base-framework', 'hooks'); fs.mkdirSync(hooksFrameworkDest, { recursive: true }); - const hooksSrcDir = path.join(src, 'src', 'hooks'); + const hooksSrcDir = path.join(src, 'hooks'); const hookFiles = fs.readdirSync(hooksSrcDir).filter(f => f.endsWith('.py')); for (const hookFile of hookFiles) { - fs.copyFileSync(path.join(hooksSrcDir, hookFile), path.join(hooksFrameworkDest, hookFile)); + copyFileExpandingMacro( + path.join(hooksSrcDir, hookFile), + path.join(hooksFrameworkDest, hookFile), + claudeDir + ); } console.log(` ${green}+${reset} base-framework/hooks/ (${hookFiles.length} hooks for scaffold)`); @@ -486,6 +485,7 @@ function installWorkspace() { } // Copy operator.json template (don't overwrite existing) + // src/templates/ is retained as the npx-mode template source (no ${CLAUDE_PLUGIN_ROOT} in JSON) const operatorJsonDest = path.join(baseDir, 'operator.json'); if (!fs.existsSync(operatorJsonDest)) { const operatorSrc = path.join(src, 'src', 'templates', 'operator.json'); @@ -508,17 +508,22 @@ function installWorkspace() { console.log(` ${green}+${reset} .base/schemas/ (${schemaFiles.length} validation schemas)`); } - // Copy base-mcp - const baseMcpSrc = path.join(src, 'src', 'packages', 'base-mcp'); + // Copy mcp/ (root tree) to .base/base-mcp/ — dest name 'base-mcp' is required + // for the vendored-location resolver in mcp/index.js (checks basename==='base-mcp'). + const baseMcpSrc = path.join(src, 'mcp'); const baseMcpDest = path.join(baseDir, 'base-mcp'); - copyDir(baseMcpSrc, baseMcpDest); + copyDir(baseMcpSrc, baseMcpDest, workspaceDir); console.log(` ${green}+${reset} .base/base-mcp/`); - // Copy all hooks to .base/hooks/ - const allHooksSrc = path.join(src, 'src', 'hooks'); + // Copy all hooks to .base/hooks/ from root hooks/ tree, expanding ${CLAUDE_PLUGIN_ROOT} + const allHooksSrc = path.join(src, 'hooks'); const hookEntries = fs.readdirSync(allHooksSrc).filter(f => f.endsWith('.py')); for (const file of hookEntries) { - fs.copyFileSync(path.join(allHooksSrc, file), path.join(baseDir, 'hooks', file)); + copyFileExpandingMacro( + path.join(allHooksSrc, file), + path.join(baseDir, 'hooks', file), + workspaceDir + ); } console.log(` ${green}+${reset} .base/hooks/ (${hookEntries.length} hooks)`); diff --git a/package.json b/package.json index 413709a..ba9e9b8 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,11 @@ }, "files": [ "bin", - "src/commands", - "src/skill", - "src/framework", - "src/packages", - "src/hooks", + "commands", + "skills", + "base-framework", + "mcp", + "hooks", "src/templates", "schemas", "README.md" diff --git a/src/commands/audit-claude-md.md b/src/commands/audit-claude-md.md deleted file mode 100644 index 5cc1c6f..0000000 --- a/src/commands/audit-claude-md.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: base:audit-claude-md -description: Audit CLAUDE.md against the CLAUDE.md Strategy and generate a compliant version -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash] ---- - - -Audit the project's CLAUDE.md for strategy compliance, interactively rewrite it section by section, and route operational rules to CARL or an artifact. - -**When to use:** "audit claude md", "check my claude.md", "rewrite my claude.md", after major workspace changes. - - - -@~/.claude/base-framework/frameworks/claudemd-strategy.md -@~/.claude/base-framework/templates/claudemd-template.md -@~/.claude/base-framework/tasks/audit-claude-md.md - - - -$ARGUMENTS - -@CLAUDE.md - - - -Follow task: @~/.claude/base-framework/tasks/audit-claude-md.md - -Key gates (do NOT skip): -1. Load strategy + template BEFORE reading user's CLAUDE.md -2. Present full audit classification — wait for user approval -3. Detect CARL — wait for user decision on rule routing -4. Propose each section individually — wait for approval per section -5. Write to CLAUDE.base.md (never overwrite CLAUDE.md) - - - -- [ ] Strategy framework loaded first -- [ ] Every line classified (KEEP/REMOVE/RESTRUCTURE/CARL_CANDIDATE) -- [ ] User approved audit before rewriting began -- [ ] CARL detection completed, rule routing decided -- [ ] Each section approved individually -- [ ] Final CLAUDE.base.md under 100 lines -- [ ] Original CLAUDE.md untouched - diff --git a/src/commands/audit-claude.md b/src/commands/audit-claude.md deleted file mode 100644 index c982a54..0000000 --- a/src/commands/audit-claude.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: base:audit-claude -description: Audit .claude/ directories across workspace for sprawl, duplication, and misalignment -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] ---- - - -Audit all .claude/ directories in this workspace. Discover sprawl, classify items against the global/workspace/project hierarchy, and remediate with operator approval at every step. - -**When to use:** "audit claude config", "clean up .claude dirs", "check claude setup", after installing global tools. - - - -@base-framework/tasks/audit-claude.md -@base-framework/frameworks/claude-config-alignment.md - - - -$ARGUMENTS - -Global config: ~/.claude/ -Workspace root config: .claude/ - - - -Follow task: @base-framework/tasks/audit-claude.md - -The framework file defines classification rules, safety protocol, and output format. -The task file defines the step-by-step process. - -Key principles: -- Every change requires operator approval -- Process from lowest risk to highest -- Never batch-delete without per-item confirmation -- Explain what, why, and what could go wrong for every change -- Copy-then-verify-then-remove, never move - - - -- [ ] All .claude/ directories discovered and inventoried -- [ ] Items classified with evidence -- [ ] Operator confirmed classifications -- [ ] Remediation executed by risk group with verification -- [ ] Summary report with manual follow-up items - diff --git a/src/commands/audit.md b/src/commands/audit.md deleted file mode 100644 index 387d54b..0000000 --- a/src/commands/audit.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: base:audit -description: Deep workspace optimization -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion] ---- - - -Deep workspace audit — comprehensive optimization across all areas with actionable recommendations. - -**When to use:** "audit my workspace", "deep clean", monthly optimization. - - - -@~/.claude/base-framework/tasks/audit.md -@~/.claude/base-framework/context/base-principles.md -@~/.claude/base-framework/frameworks/audit-strategies.md - - - -$ARGUMENTS - -@.base/workspace.json - - - -Follow task: @~/.claude/base-framework/tasks/audit.md - - - -- [ ] All areas audited with strategy-specific checks -- [ ] Findings categorized and prioritized -- [ ] Actionable recommendations presented - diff --git a/src/commands/carl-hygiene.md b/src/commands/carl-hygiene.md deleted file mode 100644 index 15ae108..0000000 --- a/src/commands/carl-hygiene.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: base:carl-hygiene -description: CARL domain maintenance and rule review -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, carl_v2_list_domains, carl_v2_get_domain, carl_v2_get_staged, carl_v2_approve_proposal, carl_v2_remove_rule, carl_v2_replace_rules, carl_v2_archive_decision] ---- - - -CARL rule lifecycle management — review staleness, staging pipeline, domain health. - -**When to use:** "carl hygiene", "review carl rules", "clean up carl". - - - -@~/.claude/base-framework/tasks/carl-hygiene.md - - - -$ARGUMENTS - -@.carl/carl.json -@.base/workspace.json - - - -Follow task: @~/.claude/base-framework/tasks/carl-hygiene.md - - - -- [ ] All domains reviewed for staleness -- [ ] Duplicate/conflicting rules identified -- [ ] Staging pipeline processed -- [ ] carl_hygiene.last_run updated in workspace.json - diff --git a/src/commands/groom.md b/src/commands/groom.md deleted file mode 100644 index 6e1aac5..0000000 --- a/src/commands/groom.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: base:groom -description: Weekly workspace maintenance cycle -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] ---- - - -Structured weekly maintenance — review each workspace area, update statuses, archive stale items, reduce drift. - -**When to use:** Weekly maintenance, "groom my workspace", "run grooming". - - - -@~/.claude/base-framework/tasks/groom.md -@~/.claude/base-framework/context/base-principles.md -@~/.claude/base-framework/frameworks/audit-strategies.md - - - -$ARGUMENTS - -@.base/workspace.json -@.base/data/state.json - - - -Follow task: @~/.claude/base-framework/tasks/groom.md - - - -- [ ] All workspace areas reviewed -- [ ] Stale items addressed -- [ ] state.json updated with groom results -- [ ] Drift score recalculated - diff --git a/src/commands/history.md b/src/commands/history.md deleted file mode 100644 index 80aedf9..0000000 --- a/src/commands/history.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: base:history -description: Workspace evolution timeline -allowed-tools: [Read, Glob, Bash] ---- - - -Show workspace evolution — grooming history, audits, major changes over time. - -**When to use:** "workspace history", "show evolution", "what's changed". - - - -@~/.claude/base-framework/tasks/history.md - - - -@.base/data/state.json - - - -Follow task: @~/.claude/base-framework/tasks/history.md - - - -- [ ] Timeline of workspace events displayed - diff --git a/src/commands/orientation.md b/src/commands/orientation.md deleted file mode 100644 index 76e1be8..0000000 --- a/src/commands/orientation.md +++ /dev/null @@ -1,87 +0,0 @@ - -## What -Guided operator identity workflow that produces or updates `.base/operator.json`. Walks the operator through Deep Why, North Star, 5 Key Values, Elevator Pitch, and Surface Vision to create a deep operator profile that aligns initiatives, projects, and tasks. - -## When to Use -- First time setting up a workspace (no operator.json exists) -- Operator feels disoriented and needs to realign -- Periodic review of identity anchors (quarterly recommended) -- After major life or business shifts - -## Not For -- Project-level planning (use /paul:plan) -- Task tracking updates (use Apex MCP directly) -- Workspace health checks (use /base:pulse) - - - -## Role -Orientation guide — facilitates deep self-inquiry without rushing, fluff, or false depth. Holds space for reflection while keeping momentum. - -## Style -- Direct questions, no leading preamble -- Waits between phases — never auto-advances -- Uses [N] option brackets for navigation at every decision point -- Reflects back what the operator said before synthesizing — no assumptions -- Challenges surface-level answers with "go deeper" follow-ups when warranted - -## Expertise -- Identity frameworks, values clarification, vision anchoring -- Composable workflow orchestration (parent/child task flow) -- Operator profile data modeling - - - -| Command | Description | Routes To | -|---------|-------------|-----------| -| `/base:orientation` | Full orientation workflow | (this entry point) | - - - -## Always Load -Nothing — lightweight until invoked. - -## Load on Command -@orientation/tasks/new-orientation.md (when no operator.json exists) -@orientation/tasks/reorientation.md (when operator.json exists and operator wants to reset) - -## Load on Demand -@orientation/tasks/deep-why.md (Phase 1 of orientation) -@orientation/tasks/north-star.md (Phase 2 of orientation) -@orientation/tasks/key-values.md (Phase 3 of orientation) -@orientation/tasks/elevator-pitch.md (Phase 4 of orientation) -@orientation/tasks/surface-vision.md (Phase 5 of orientation) -@orientation/tasks/initiatives.md (Phase 6 of orientation) -@orientation/tasks/project-mapping.md (Phase 7 of orientation) -@orientation/tasks/task-seeding.md (Phase 8 of orientation) -@orientation/templates/operator-json.md (schema reference for operator.json) - - - -## Orientation Check - -Read `.base/operator.json` to determine current state. - -**If operator.json does NOT exist:** -> No operator profile found. This is your first orientation. -> -> This workflow will walk you through 8 phases — 5 identity exercises then 3 workspace alignment steps. Defines who you are, where you're headed, and organizes your work to match. Takes 30-60 minutes depending on how deep you go. -> -> **[1] Begin orientation** -> **[2] Not now — exit** - -If [1]: Load and execute @orientation/tasks/new-orientation.md - -**If operator.json EXISTS:** -Load and display current profile summary (North Star, Deep Why, Values, Pitch, Vision — one line each). - -> Your last orientation was on {last_updated date}. -> -> **[1] Full reorientation** — reset everything from scratch -> **[2] Update a specific section** — keep the rest -> **[3] Review only** — just look, don't change anything - -If [1]: Load and execute @orientation/tasks/reorientation.md -If [2]: Ask which section, then load that specific task -If [3]: Display full profile and exit - diff --git a/src/commands/orientation/tasks/deep-why.md b/src/commands/orientation/tasks/deep-why.md deleted file mode 100644 index 1aac34c..0000000 --- a/src/commands/orientation/tasks/deep-why.md +++ /dev/null @@ -1,132 +0,0 @@ - -Excavate the operator's root motivation through a 5-layer "but why?" process. Each layer peels back a surface answer to find the one beneath it, arriving at a foundational Deep Why statement. - - - -As an operator, I want to uncover my deepest motivation for doing what I do, so that my work connects to something real and sustaining rather than external pressure or habit. - - - -- Phase 1 of new orientation -- Operator chose to reorient their Deep Why specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -> Your previous Deep Why was: -> *"{previous statement}"* -> -> We're going to excavate fresh. You can land in the same place or somewhere new. Either is fine. - - -> **Layer 1 of 5** -> -> Why do you do what you do? Not the business answer. Not the resume answer. The real one. - -**Wait for response.** - - - -Reflect back what the operator said in Layer 1 — one sentence, their words not yours. - -> You said: "{reflection}" -> -> **Layer 2 of 5** -> -> But why does that matter to you? - -**Wait for response.** - - - -Reflect back Layer 2. - -> You said: "{reflection}" -> -> **Layer 3 of 5** -> -> But why? - -If the answer feels surface-level or performative, push: "That sounds like something you'd say to someone else. What's the version you'd say to yourself at 2am?" - -**Wait for response.** - - - -Reflect back Layer 3. - -> You said: "{reflection}" -> -> **Layer 4 of 5** -> -> Why does that drive you more than anything else? - -**Wait for response.** - - - -Reflect back Layer 4. - -> You said: "{reflection}" -> -> **Layer 5 of 5 — the root** -> -> If everything else was stripped away — the business, the tools, the audience — what's left? What's the thing that would still make you get up and build? - -**Wait for response.** - - - -Now synthesize all 5 layers into a single Deep Why statement. - -1. Read back all 5 layers -2. Draft a one-sentence synthesis that captures the root, not just Layer 5 but the thread through all layers -3. Present it: - -> Here are your 5 layers: -> 1. {layer 1} -> 2. {layer 2} -> 3. {layer 3} -> 4. {layer 4} -> 5. {layer 5} -> -> **Your Deep Why:** -> *"{synthesized statement}"* -> -> **[1] That's it** — lock it in -> **[2] Close but needs tweaking** — let me adjust the wording -> **[3] Not right** — let me redo a layer - -If [2]: Let operator edit the statement directly. -If [3]: Ask which layer to redo, then resume from that layer. - -**Wait for response.** - - - -Deep Why is locked. - -Return to parent workflow with: -- `layers`: array of 5 layer answers -- `statement`: final approved synthesis -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Deep Why data: 5 layers + synthesized statement, ready for operator.json. - - - -- [ ] All 5 layers answered by operator (not generated) -- [ ] Each layer reflected back before asking the next -- [ ] Surface answers challenged when detected -- [ ] Synthesis captures the thread, not just layer 5 -- [ ] Operator approved the final statement - diff --git a/src/commands/orientation/tasks/elevator-pitch.md b/src/commands/orientation/tasks/elevator-pitch.md deleted file mode 100644 index 41dc5c9..0000000 --- a/src/commands/orientation/tasks/elevator-pitch.md +++ /dev/null @@ -1,115 +0,0 @@ - -Synthesize the operator's identity into a 4-floor elevator pitch — a 30-second statement of who they are, what they do, why it matters, and where they're going. Built on top of Deep Why, North Star, and Values. - - - -As an operator, I want a clear, statable pitch for who I am and what I'm about, so that I can articulate my value to anyone in 30 seconds without fumbling. - - - -- Phase 4 of new orientation (after Key Values) -- Operator chose to reorient their Elevator Pitch specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -> Your previous pitch was: -> *"{previous pitch}"* - - -> Your foundation so far: -> - **Deep Why:** {statement} -> - **North Star:** {metric} -> - **Values:** {v1}, {v2}, {v3}, {v4}, {v5} -> -> An elevator pitch has 4 floors. Each floor is one sentence: -> -> **Floor 1** — Who you are (identity, not job title) -> **Floor 2** — What you do (the work, in plain language) -> **Floor 3** — Why it matters (the impact) -> **Floor 4** — What's next (the vision or the ask) -> -> Let's build each floor. Don't try to be clever — try to be clear. - - - -> **Floor 1: Who are you?** -> -> Not your job title. Not your company. If you met someone at a party and they asked "what are you about?" — what's the one-sentence answer? - -**Wait for response.** - - - -Reflect Floor 1 back, then: - -> **Floor 2: What do you do?** -> -> The work itself. What does your day look like when it's going well? One sentence. - -**Wait for response.** - - - -Reflect Floor 2 back, then: - -> **Floor 3: Why does it matter?** -> -> Who benefits and how? This connects to your Deep Why. One sentence. - -**Wait for response.** - - - -Reflect Floor 3 back, then: - -> **Floor 4: What's next?** -> -> Where is this going? What's the thing you're building toward? This connects to your North Star. One sentence. - -**Wait for response.** - - - -Assemble the full pitch from all 4 floors. Read it as one continuous statement. - -> ## Your Elevator Pitch -> -> *"{floor1} {floor2} {floor3} {floor4}"* -> -> Read it out loud. Does it sound like you? -> -> **[1] That's me — lock it in** -> **[2] Reword a floor** — tell me which one -> **[3] Start over** — the whole thing feels off - -**Wait for response.** - - - -Elevator Pitch is locked. - -Return to parent workflow with: -- `pitch`: full assembled pitch -- `floors`: object with floor_1 through floor_4 -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Elevator Pitch data: 4 floors + assembled pitch, ready for operator.json. - - - -- [ ] Each floor answered individually by operator -- [ ] Full pitch reads as one coherent 30-second statement -- [ ] Floor 3 connects to Deep Why -- [ ] Floor 4 connects to North Star -- [ ] Operator confirmed it sounds like them - diff --git a/src/commands/orientation/tasks/initiatives.md b/src/commands/orientation/tasks/initiatives.md deleted file mode 100644 index d3cd31a..0000000 --- a/src/commands/orientation/tasks/initiatives.md +++ /dev/null @@ -1,98 +0,0 @@ - -Guide the operator through defining or reviewing goal-oriented initiatives aligned to their North Star. Each initiative is a measurable strategic objective, not an entity or project. - - - -As an operator who has completed their identity profile, I want to define the strategic objectives my work serves, so that every project and task traces back to a clear goal. - - - -- Phase 6 of new orientation (after Surface Vision) -- Operator chose to reorient their initiatives specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -Pull current initiatives via base_list_projects(type="initiative") and display them: - -> ## Current Initiatives -> -> {For each initiative: title, description, status, project count} -> -> We'll review each one against your North Star. - - - -> Your North Star: *"{north_star_metric}"* -> Your Deep Why: *"{deep_why_statement}"* -> -> Initiatives are the strategic objectives that move you toward the North Star. NEVER confuse them with businesses or entities — an initiative is a measurable goal. -> -> Example: Not "C&C Strategic Consulting" but "Build C&C to $7k/month MRR" -> -> What are the 2-4 major objectives you're working toward right now? Think in terms of outcomes, not labels. - - -**Wait for response.** - - - -For each initiative the operator names: - -> **Initiative: "{title}"** -> -> 1. How does this connect to your North Star? -> 2. What's the key metric or success criteria? -> 3. Is there a timeframe? - -Capture: title, description, priority, category, metric, timeframe. - -NEVER let an initiative through that is actually an entity (person, business, org). Push back: "That's a business name, not a goal. What's the measurable objective for that business?" - -**Wait for response between each initiative.** - - - -Present all initiatives: - -> ## Your Initiatives -> -> 1. **{title}** — {description} ({metric}, {timeframe}) -> 2. **{title}** — {description} ({metric}, {timeframe}) -> ... -> -> **[1] Lock these in** -> **[2] Add another** -> **[3] Edit one** -> **[4] Remove one** - -When locked, write each initiative via base_add_project(type="initiative") with full metadata. - -**Wait for response.** - - - -Initiatives are locked. - -Return to parent workflow with: -- `initiatives`: array of created initiative IDs and titles -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Initiatives created in Apex via MCP. Each has title, description, priority, category, metric alignment to North Star. - - - -- [ ] Each initiative is a measurable goal, not an entity -- [ ] Each initiative connects to the North Star -- [ ] Written via base_add_project MCP, not manual file edits -- [ ] Operator approved the final set - diff --git a/src/commands/orientation/tasks/key-values.md b/src/commands/orientation/tasks/key-values.md deleted file mode 100644 index cb4a634..0000000 --- a/src/commands/orientation/tasks/key-values.md +++ /dev/null @@ -1,130 +0,0 @@ - -Guide the operator through identifying and ranking their top 5 personal values — the core principles they operate by. The constraint of exactly 5 forces prioritization and clarity. - - - -As an operator, I want to be clear on my top 5 operating principles, so that I can make decisions faster and stay aligned when things get chaotic. - - - -- Phase 3 of new orientation (after North Star) -- Operator chose to reorient their Key Values specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -> Your previous values were: -> 1. {v1} — {meaning1} -> 2. {v2} — {meaning2} -> 3. {v3} — {meaning3} -> 4. {v4} — {meaning4} -> 5. {v5} — {meaning5} - - -> Your Deep Why: *"{deep_why_statement}"* -> Your North Star: *"{north_star_metric}"* -> -> Values are the principles you actually operate by — not aspirational ones you wish you had. Think about the last time you made a hard decision. What did you lean on? -> -> Give me 7-10 values that feel true. Don't overthink it — we'll narrow down. - -**Wait for response.** - - - -Before cutting, reflect each value back with a personalized one-line description based on what you know about the operator. This helps them see what each value actually means to them, not just the word. - -> Here's what I'm hearing from you: -> -> 1. **{Value}** — {personalized description based on context} -> 2. **{Value}** — {personalized description} -> ... -> -> Is anything missing that you'd fight for if challenged? Or are these the ones? -> -> **[1] These are the ones — let's cut to 5** -> **[2] I want to add 1-2 more, then cut** - -**Wait for response.** - - - -Force the cut. ALWAYS show the personalized description alongside each value — never list bare value names. - -> Now the hard part — cut to 5. The ones that survive are the ones you'd defend if someone challenged them. -> -> Which ones can you live without? Drop them one at a time until you're at 5. - -If operator struggles, offer a forcing question: "If you could only teach your kids 5 principles about how to live, which of these make the cut?" - -**Wait for response.** - - - -Present values WITH their descriptions, then ask for ranking: - -> 1. **{Value}** — {description} -> 2. **{Value}** — {description} -> ... -> -> Now rank them 1-5. #1 is the one that wins when two values conflict. - -**Wait for response.** - - - -For each value in ranked order: - -> **{Value #N}: {value name}** -> In one sentence — what does this look like in practice for you? Not the dictionary definition. How does this value show up in your daily decisions? - -Iterate through all 5, waiting for each response. - -**Wait for each response.** - - - -Present the complete values list: - -> ## Your 5 Key Values -> -> 1. **{v1}** — {meaning} -> 2. **{v2}** — {meaning} -> 3. **{v3}** — {meaning} -> 4. **{v4}** — {meaning} -> 5. **{v5}** — {meaning} -> -> **[1] Lock it in** -> **[2] Swap a value** -> **[3] Rerank** -> **[4] Reword a meaning** - -**Wait for response.** - - - -Key Values are locked. - -Return to parent workflow with: -- `values`: array of 5 objects (rank, value, meaning) -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Key Values data: 5 ranked values with practical meanings, ready for operator.json. - - - -- [ ] Started with 7-10 brainstormed values -- [ ] Narrowed to exactly 5 -- [ ] Ranked 1-5 with explicit prioritization -- [ ] Each value has a practical meaning (not dictionary definition) -- [ ] Operator approved the final list - diff --git a/src/commands/orientation/tasks/new-orientation.md b/src/commands/orientation/tasks/new-orientation.md deleted file mode 100644 index 8553ae6..0000000 --- a/src/commands/orientation/tasks/new-orientation.md +++ /dev/null @@ -1,162 +0,0 @@ - -Orchestrate a full first-time orientation through all 5 phases: Deep Why, North Star, Key Values, Elevator Pitch, Surface Vision. Composes each phase task sequentially, passing context forward, and produces operator.json at the end. - - - -As an operator setting up my workspace for the first time, I want a guided walkthrough of my identity anchors, so that my initiatives, projects, and tasks align to who I am and where I'm going. - - - -- No operator.json exists yet -- Entry point routes here when operator chooses [1] Begin orientation - - - -@../templates/operator-json.md - - - - - -Brief the operator on what's coming. - -> You'll go through 5 exercises. Each builds on the last: -> -> 1. **Deep Why** — excavate your root motivation (5 layers) -> 2. **North Star** — define the one metric everything points at -> 3. **Key Values** — narrow to your top 5 operating principles -> 4. **Elevator Pitch** — synthesize who you are in 30 seconds -> 5. **Surface Vision** — anchor a tangible picture of the future -> -> Take your time. There's no wrong answers, but surface-level ones won't serve you. -> -> **[1] Let's go — start with Deep Why** -> **[2] I need a minute — come back to this** - -If [2]: Exit gracefully. Orientation can resume anytime via `/base:orientation`. - -**Wait for response.** - - - -Load and execute @deep-why.md - -Pass no prior context (this is the first phase). - -When deep-why.md completes and locks its result, capture the output (layers + statement) and continue here. - - - -Load and execute @north-star.md - -Pass context: the Deep Why statement from Phase 1. - -When north-star.md completes and locks its result, capture the output (metric + timeframe + rationale) and continue here. - - - -Load and execute @key-values.md - -Pass context: Deep Why statement + North Star metric. - -When key-values.md completes and locks its result, capture the output (5 ranked values with meanings) and continue here. - - - -Load and execute @elevator-pitch.md - -Pass context: Deep Why statement + North Star metric + Key Values list. - -When elevator-pitch.md completes and locks its result, capture the output (4 floors + full pitch) and continue here. - - - -Load and execute @surface-vision.md - -Pass context: Deep Why statement + North Star metric + Key Values + Elevator Pitch. - -When surface-vision.md completes and locks its result, capture the output (scenes + summary) and continue here. - - - -All 5 phases complete. Synthesize into operator.json. - -1. Read @../templates/operator-json.md for schema -2. Populate all fields from captured phase outputs -3. Write to `.base/operator.json` -4. Display the complete profile in a clean summary: - -> ## Your Operator Profile -> -> **Deep Why:** {statement} -> **North Star:** {metric} ({timeframe}) -> **Values:** {value1}, {value2}, {value3}, {value4}, {value5} -> **Elevator Pitch:** {full pitch} -> **Surface Vision:** {summary} -> -> Profile saved to `.base/operator.json`. -> -> **[1] Looks right — done** -> **[2] Something's off — let me adjust a section** - -If [2]: Ask which section, load that specific task for revision, then re-write operator.json. - -**Wait for response.** - - - -Load and execute @initiatives.md - -Pass context: Full operator profile (Deep Why, North Star, Values). - -When initiatives.md completes and locks its result, capture the output (initiative IDs and titles) and continue here. - - - -Load and execute @project-mapping.md - -Pass context: Created initiatives from Phase 6. - -When project-mapping.md completes and locks its result, capture the output (mapping counts, PAUL sync counts) and continue here. - - - -Load and execute @task-seeding.md - -Pass context: Initiative → Project mapping from Phase 7. - -When task-seeding.md completes and locks its result, capture the output (task counts) and continue here. - - - -All 8 phases complete. Display final summary: - -> ## Orientation Complete -> -> **Operator Profile:** `.base/operator.json` -> - Deep Why, North Star, Values, Pitch, Vision — locked -> -> **Initiatives:** {count} defined, aligned to North Star -> **Projects:** {mapped_count} mapped to initiatives, {unparented_count} unparented -> **PAUL Satellites:** {paul_synced} synced -> **Tasks:** {tasks_created} seeded across {projects_with_tasks} projects -> -> You're oriented. Run `/base:orientation` anytime to review or reorient. - - - - - -Complete workspace orientation: operator.json populated, initiatives defined, projects mapped, PAUL synced, tasks seeded. - - - -- [ ] All 8 phase tasks executed in order -- [ ] Each phase locked before advancing to next -- [ ] Context passed forward between phases -- [ ] operator.json written with all fields populated -- [ ] Initiatives created via Apex MCP -- [ ] Projects mapped to initiatives with PAUL data synced -- [ ] Tasks seeded under projects -- [ ] Operator reviewed and approved at each phase - diff --git a/src/commands/orientation/tasks/north-star.md b/src/commands/orientation/tasks/north-star.md deleted file mode 100644 index ebef59b..0000000 --- a/src/commands/orientation/tasks/north-star.md +++ /dev/null @@ -1,97 +0,0 @@ - -Define the operator's North Star — the one key metric or outcome that everything else aligns toward. Derives from the Deep Why and gives initiatives a measurable target. - - - -As an operator, I want a single guiding metric that tells me whether my work is pointing in the right direction, so that I can evaluate opportunities and say no to misaligned ones. - - - -- Phase 2 of new orientation (after Deep Why) -- Operator chose to reorient their North Star specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -> Your previous North Star was: -> *"{previous metric}" ({previous timeframe})* - - -> Your Deep Why: *"{deep_why_statement}"* -> -> A North Star is the one metric or outcome that, if you achieved it, would mean your Deep Why is being lived. It's not a task. It's not a project. It's the thing that makes all the projects make sense. -> -> What's the one outcome that matters most to you right now? Think in terms of something you could measure or clearly evaluate. - -**Wait for response.** - - - -Evaluate the response: - -- If too vague ("be successful", "make an impact"): Push for specificity. "What would you point to as proof that's happening?" -- If too narrow ("launch CaseGate"): Push for altitude. "That's a project, not a star. What does completing that project serve?" -- If it's a feeling ("feel free"): Acknowledge it, then ask "What would be true in your life when you feel that? What's the measurable version?" - -Once the metric is crisp: - -> **Your North Star metric:** -> *"{metric}"* -> -> What timeframe feels right for this? Not a deadline — a horizon. When would you evaluate whether you're on track? -> -> **[1] 6 months** -> **[2] 1 year** -> **[3] 2-3 years** -> **[4] Custom — I'll specify** - -**Wait for response.** - - - -> Last piece — why this metric above all the others you could have chosen? One sentence. - -**Wait for response.** - - - -> **Your North Star:** -> *"{metric}"* -> **Timeframe:** {timeframe} -> **Why this one:** {rationale} -> -> **[1] Lock it in** -> **[2] Adjust the metric** -> **[3] Adjust the timeframe** - -**Wait for response.** - - - -North Star is locked. - -Return to parent workflow with: -- `metric`: the north star metric -- `timeframe`: chosen horizon -- `rationale`: why this metric -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -North Star data: metric + timeframe + rationale, ready for operator.json. - - - -- [ ] Metric is specific and evaluable (not vague aspiration) -- [ ] Metric connects back to Deep Why -- [ ] Timeframe is set -- [ ] Rationale captured -- [ ] Operator approved the final North Star - diff --git a/src/commands/orientation/tasks/project-mapping.md b/src/commands/orientation/tasks/project-mapping.md deleted file mode 100644 index 8744cb2..0000000 --- a/src/commands/orientation/tasks/project-mapping.md +++ /dev/null @@ -1,103 +0,0 @@ - -Map existing projects to their parent initiatives and sync PAUL satellite data. Every project should trace to an initiative or be explicitly unparented. - - - -As an operator with defined initiatives, I want my projects organized under the right strategic objectives with current PAUL data, so that I can see which work serves which goal. - - - -- Phase 7 of new orientation (after Initiatives) -- Operator wants to reorganize project-to-initiative mappings -- Composed by new-orientation.md or reorientation.md - - - - - -Pull initiatives and projects in parallel: -- base_list_projects(type="initiative") -- base_list_projects(type="project") - -Display initiatives as target buckets, then list all projects with their current parent_id (or "unparented"). - -> ## Initiatives (target buckets) -> {For each: ID, title} -> -> ## Projects to Map -> {For each: ID, title, current parent, status, PAUL satellite if any} -> -> I'll go through each project. Tell me which initiative it belongs under, or say "none" to leave it unparented. - - - -For each project without a parent (or with a stale/wrong parent): - -> **{PRJ-ID}: {title}** ({status}) -> {PAUL: satellite_name, phase if applicable} -> -> Which initiative does this serve? -> {List initiative options as [N] brackets} -> **[N+1] None — leave unparented** -> **[N+2] Archive — no longer active** - -**Wait for response before moving to next project.** - -When assigned, update via base_update_project(id, {parent_id: "INI-XXX"}). - - - -For each project that has a PAUL satellite (paul.json exists in its location): - -1. Read the paul.json file from the project's location -2. Update the project's paul field with current: satellite_name, location, milestone, phase, loop_position, handoff status and path -3. Update via base_update_project - -Report: "{N} PAUL satellites synced" - - - -Display the final mapping: - -> ## Initiative → Project Mapping -> -> **{INI-001}: {title}** -> - {PRJ-XXX}: {title} (PAUL: {phase} | {status}) -> - ... -> -> **{INI-002}: {title}** -> - ... -> -> **Unparented:** -> - {PRJ-XXX}: {title} -> -> **[1] Looks right — lock it in** -> **[2] Move a project** - -**Wait for response.** - - - -Project mapping is locked. - -Return to parent workflow with: -- `mapped_count`: number of projects assigned to initiatives -- `unparented_count`: number left without a parent -- `paul_synced`: number of PAUL satellites updated -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -All projects mapped to initiatives via Apex MCP. PAUL satellite data synced from paul.json files. - - - -- [ ] Every project reviewed — assigned to initiative, left unparented, or archived -- [ ] Parent IDs set via base_update_project MCP -- [ ] PAUL satellites synced from source paul.json files -- [ ] Final mapping displayed and approved by operator - diff --git a/src/commands/orientation/tasks/reorientation.md b/src/commands/orientation/tasks/reorientation.md deleted file mode 100644 index d6ed4e5..0000000 --- a/src/commands/orientation/tasks/reorientation.md +++ /dev/null @@ -1,96 +0,0 @@ - -Guide an operator through reorientation when an existing operator.json is present. Iterates through each section, showing current values and prompting for keep/reset decisions before composing relevant phase tasks. - - - -As an operator who has been through orientation before, I want to selectively reset parts of my identity profile, so that my profile evolves with me without starting completely from scratch every time. - - - -- operator.json already exists -- Operator chose [1] Full reorientation from entry point - - - -@../templates/operator-json.md - - - - - -Read `.base/operator.json` and display the full current profile. - -> ## Current Operator Profile -> -> **Deep Why:** {current statement} -> *(Last set: {date})* -> -> **North Star:** {current metric} ({timeframe}) -> *(Last set: {date})* -> -> **Values:** {v1}, {v2}, {v3}, {v4}, {v5} -> *(Last set: {date})* -> -> **Elevator Pitch:** {current pitch} -> *(Last set: {date})* -> -> **Surface Vision:** {current summary} -> *(Last set: {date})* - -Then begin iterating through each section. - - - -For each section in order (Deep Why, North Star, Key Values, Elevator Pitch, Surface Vision, Initiatives, Project Mapping, Task Seeding): - -> **{Section Name}** -> Current: {current value — one-line summary} -> Last set: {date} -> -> **[1] Keep** — this still resonates -> **[2] Reorient** — this needs work -> **[3] Skip for now** — come back to it later - -**Wait for response before moving to next section.** - -If [2]: Load and execute the corresponding phase task (e.g., @deep-why.md for Deep Why, @initiatives.md for Initiatives, @project-mapping.md for Project Mapping, @task-seeding.md for Task Seeding), passing current value as "previous orientation" context so the operator can see what they had before. When phase task completes, capture new output and continue iteration. - -If [1] or [3]: Keep current value, move to next section. - -Track which sections were reoriented vs kept. - - - -After iterating all 8 sections: - -1. Merge kept values with new values from reoriented sections -2. Update `last_updated` timestamp -3. Update `completed_at` only for sections that were reoriented -4. Write updated `.base/operator.json` -5. Display summary showing what changed: - -> ## Reorientation Complete -> -> **Changed:** {list of reoriented sections} -> **Kept:** {list of kept sections} -> **Skipped:** {list of skipped sections} -> -> Profile updated at `.base/operator.json`. - -**Wait for acknowledgment.** - - - - - -Updated `.base/operator.json` with selectively reoriented sections. - - - -- [ ] Current profile loaded and displayed -- [ ] Each section presented with keep/reorient/skip options -- [ ] Reoriented sections went through their full phase task -- [ ] Kept sections preserved unchanged -- [ ] operator.json updated with merged results -- [ ] Summary shows what changed vs what stayed - diff --git a/src/commands/orientation/tasks/surface-vision.md b/src/commands/orientation/tasks/surface-vision.md deleted file mode 100644 index 3235ca6..0000000 --- a/src/commands/orientation/tasks/surface-vision.md +++ /dev/null @@ -1,113 +0,0 @@ - -Guide the operator to conjure 2-5 concrete, tangible future moments that represent what their life looks like when things are working. These are sensory anchors — specific enough to visualize, "superficial" on purpose because the surface is what the inner world connects to. - - - -As an operator, I want tangible anchor points for the future I'm building toward, so that my inner psychology has something concrete to orient around rather than abstract goals. - - - -- Phase 5 of new orientation (after Elevator Pitch) -- Operator chose to reorient their Surface Vision specifically -- Composed by new-orientation.md or reorientation.md - - - - - - -> Your previous Surface Vision scenes: -> - {scene 1} -> - {scene 2} -> - {scene 3} -> Summary: *"{previous summary}"* - - -> Your foundation: -> - **Deep Why:** {statement} -> - **North Star:** {metric} -> - **Values:** {v1}, {v2}, {v3}, {v4}, {v5} -> - **Pitch:** {pitch} -> -> Surface Vision is different from the others. This isn't about metrics or statements. This is about moments. -> -> Close your eyes for a second. Picture your life when the North Star is hit and the Deep Why is being lived daily. Don't think about the business model or the revenue. Think about a single moment in a regular day. -> -> What does one specific moment look like? Be concrete — where are you, what are you doing, what do you see, hear, feel? - -**Wait for response.** - - - -After the first scene: - -> Good. That's Scene 1. -> -> Give me another moment. Different context — maybe a different time of day, different setting, different people. Still concrete, still specific. - -**Wait for response.** - -Continue capturing until operator has 2-5 scenes. After each: - -> **[1] Add another scene** (max 5) -> **[2] That's enough — move on** - -**Wait for response.** - - - -Read back all scenes, then: - -> Your scenes: -> 1. {scene 1} -> 2. {scene 2} -> 3. {scene 3} -> ... -> -> What's the thread? If you had to capture the essence of these moments in one sentence — what's the surface vision? - -**Wait for response.** - -If the summary is too abstract, push: "Make it more concrete. What would a camera capture?" - - - -> ## Your Surface Vision -> -> **Scenes:** -> 1. {scene 1} -> 2. {scene 2} -> 3. {scene 3} -> -> **Summary:** *"{summary}"* -> -> **[1] Lock it in** -> **[2] Add or replace a scene** -> **[3] Reword the summary** - -**Wait for response.** - - - -Surface Vision is locked. - -Return to parent workflow with: -- `scenes`: array of scene strings -- `summary`: one-sentence synthesis -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Surface Vision data: 2-5 scenes + summary sentence, ready for operator.json. - - - -- [ ] At least 2 scenes captured (max 5) -- [ ] Each scene is concrete and sensory, not abstract -- [ ] Summary captures the thread across scenes -- [ ] Operator approved the final vision - diff --git a/src/commands/orientation/tasks/task-seeding.md b/src/commands/orientation/tasks/task-seeding.md deleted file mode 100644 index b468ff6..0000000 --- a/src/commands/orientation/tasks/task-seeding.md +++ /dev/null @@ -1,93 +0,0 @@ - -Seed initial tasks under projects after initiatives and project mapping are complete. Tasks are the operator's personal accountability items — concrete must-dos regardless of who or how. - - - -As an operator with aligned initiatives and projects, I want to capture the immediate must-do items under each project, so that I have a clear picture of what needs to happen next. - - - -- Phase 8 of new orientation (after Project Mapping) -- Operator wants to refresh their task list -- Composed by new-orientation.md or reorientation.md - - - - - -> Tasks are YOUR accountability items — things that must get done. Not Claude Code todos. Not aspirational ideas. Concrete next actions. -> -> We'll go initiative by initiative, project by project. For each project, I'll show you the current status and ask: "What must get done next?" -> -> You can skip any project that doesn't need tasks right now. -> -> **[1] Let's go — start with the highest priority initiative** -> **[2] Skip task seeding for now** - -If [2]: Exit gracefully. Tasks can be added anytime via Apex MCP. - -**Wait for response.** - - - -For each initiative (highest priority first): - -> ## {INI-ID}: {initiative title} - -For each project under the initiative: - -> **{PRJ-ID}: {project title}** ({status}) -> Next: {current next action from project data} -> {Blocked: {blocker} if applicable} -> -> Any must-do tasks to log under this project? -> Type them out, or say "skip" to move on. - -**Wait for response.** - -For each task the operator names: -- Create via base_add_project(type="task", parent_id="{PRJ-ID}", title="{task}") -- Confirm: "Logged: {task} under {project}" - -Move to next project after each response. - - - -After iterating all initiatives and projects: - -> ## Tasks Seeded -> -> {For each initiative → project → tasks created} -> -> **Total:** {N} tasks across {N} projects -> -> **[1] Done — lock it in** -> **[2] Add more to a specific project** - -**Wait for response.** - - - -Task seeding is locked. - -Return to parent workflow with: -- `tasks_created`: total count -- `projects_with_tasks`: count of projects that received tasks -- `completed_at`: current ISO date - -This phase is complete. Parent workflow resumes. - - - - - -Tasks created in Apex via MCP under their parent projects. Operator's immediate accountability items are captured. - - - -- [ ] Each initiative's projects presented for task seeding -- [ ] Tasks created via base_add_project(type="task") with correct parent_id -- [ ] Operator could skip projects freely -- [ ] Summary displayed with total counts -- [ ] NEVER treated tasks as Claude Code internal todos - diff --git a/src/commands/orientation/templates/operator-json.md b/src/commands/orientation/templates/operator-json.md deleted file mode 100644 index 195a721..0000000 --- a/src/commands/orientation/templates/operator-json.md +++ /dev/null @@ -1,88 +0,0 @@ -# Operator Profile Template - -Output file: `.base/operator.json` - -```template -{ - "version": 1, - "last_updated": "{iso-date}", - "hook_active": true, - "operator": { - "entity_id": "{apex-entity-id}", - "name": "{operator-name}" - }, - "deep_why": { - "layers": [ - { "level": 1, "question": "Why do you do what you do?", "answer": "[Layer 1 answer]" }, - { "level": 2, "question": "But why does that matter?", "answer": "[Layer 2 answer]" }, - { "level": 3, "question": "But why?", "answer": "[Layer 3 answer]" }, - { "level": 4, "question": "But why?", "answer": "[Layer 4 answer]" }, - { "level": 5, "question": "But why?", "answer": "[Layer 5 answer — the root]" } - ], - "statement": "[Synthesized deep why — one sentence distilled from the 5 layers]", - "completed_at": "{iso-date}" - }, - "north_star": { - "metric": "[The key metric or outcome everything aligns toward]", - "timeframe": "[Target timeframe for this north star]", - "rationale": "[Why this metric above all others]", - "completed_at": "{iso-date}" - }, - "key_values": { - "values": [ - { "rank": 1, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, - { "rank": 2, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, - { "rank": 3, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, - { "rank": 4, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" }, - { "rank": 5, "value": "[Value name]", "meaning": "[What this means to the operator in practice]" } - ], - "completed_at": "{iso-date}" - }, - "elevator_pitch": { - "pitch": "[4-floor elevator pitch — who you are, what you do, why it matters, what's next]", - "floors": { - "floor_1": "[Who you are]", - "floor_2": "[What you do]", - "floor_3": "[Why it matters]", - "floor_4": "[What's next / the ask / the vision]" - }, - "completed_at": "{iso-date}" - }, - "surface_vision": { - "scenes": [ - "[Concrete future moment — specific, sensory, tangible]", - "[Another concrete future moment]", - "[Another concrete future moment]" - ], - "summary": "[One sentence that captures the overall surface vision]", - "completed_at": "{iso-date}" - }, - "extensions": {} -} -``` - -## Field Documentation - -| Field | Type | Description | -|-------|------|-------------| -| `version` | integer | Schema version for future migrations | -| `last_updated` | ISO date | When any section was last modified | -| `hook_active` | boolean | Controls whether operator hook injects context per prompt | -| `operator.entity_id` | string | Links to Apex entity (e.g., ENT-001) | -| `deep_why.layers` | array | The 5-layer "but why?" excavation | -| `deep_why.statement` | string | Final synthesized deep why | -| `north_star.metric` | string | The one metric/outcome that matters most | -| `north_star.timeframe` | string | When this should be achieved | -| `key_values.values` | array | Ranked top 5, each with practical meaning | -| `elevator_pitch.floors` | object | 4-part structured pitch | -| `surface_vision.scenes` | array | 2-5 concrete future moments | -| `extensions` | object | Open field for future operator metadata | - -## Section Specifications - -- **deep_why**: All 5 layers must be filled. The statement is a synthesis, not a copy of layer 5. -- **north_star**: Must be measurable or at minimum clearly evaluable. Timeframe is required. -- **key_values**: Exactly 5, ranked. Meaning field captures how the value shows up in daily decisions. -- **elevator_pitch**: Each floor is one sentence max. The full pitch should be speakable in 30 seconds. -- **surface_vision**: Minimum 2 scenes, maximum 5. Must be concrete and sensory, not abstract aspirations. -- **extensions**: Reserved for future operator metadata (e.g., strengths profile, archetype data). diff --git a/src/commands/pulse.md b/src/commands/pulse.md deleted file mode 100644 index 97cedf3..0000000 --- a/src/commands/pulse.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: base:pulse -description: Daily workspace health briefing -allowed-tools: [Read, Glob, Grep, Bash] ---- - - -Workspace health briefing — drift score, stale areas, overdue grooming, quick status. - -**When to use:** Session start, "what's the state of my workspace", daily check-in. - - - -@~/.claude/base-framework/tasks/pulse.md -@~/.claude/base-framework/context/base-principles.md - - - -$ARGUMENTS - -@.base/workspace.json -@.base/data/state.json - - - -Follow task: @~/.claude/base-framework/tasks/pulse.md - - - -- [ ] Drift score calculated and displayed -- [ ] Stale areas identified -- [ ] Groom cadence checked - diff --git a/src/commands/scaffold.md b/src/commands/scaffold.md deleted file mode 100644 index a8271b6..0000000 --- a/src/commands/scaffold.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: base:scaffold -description: Set up BASE in a new workspace -argument-hint: "[--full]" -allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] ---- - - -Guided workspace setup — scan, configure, install BASE infrastructure. Optional --full mode adds operational templates. - -**When to use:** First-time BASE installation, "set up base", "scaffold my workspace". - - - -@~/.claude/base-framework/tasks/scaffold.md -@~/.claude/base-framework/templates/workspace-json.md -@~/.claude/base-framework/templates/workspace-json.md - - - -$ARGUMENTS - - - -Follow task: @~/.claude/base-framework/tasks/scaffold.md - - - -- [ ] .base/ directory structure created -- [ ] workspace.json generated from scan -- [ ] state.json initialized -- [ ] Hooks and MCP servers installed (if --full) - diff --git a/src/commands/status.md b/src/commands/status.md deleted file mode 100644 index 63cea08..0000000 --- a/src/commands/status.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: base:status -description: Quick workspace health check -allowed-tools: [Read, Glob, Bash] ---- - - -One-liner workspace health status — drift score and area summary. - -**When to use:** Quick check, "workspace status", "how's my workspace". - - - -@~/.claude/base-framework/tasks/status.md - - - -@.base/workspace.json -@.base/data/state.json - - - -Follow task: @~/.claude/base-framework/tasks/status.md - - - -- [ ] Health status displayed - diff --git a/src/commands/surface-convert.md b/src/commands/surface-convert.md deleted file mode 100644 index 42d2de8..0000000 --- a/src/commands/surface-convert.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: base:surface-convert -description: Convert a markdown file into a data surface -argument-hint: "" -allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] ---- - - -Convert an existing @-mentioned markdown file into a structured data surface. Analyzes structure, proposes schema, migrates content. - -**When to use:** User has a markdown file they want to convert to a structured surface. - - - -@~/.claude/base-framework/tasks/surface-convert.md -@.base/hooks/_template.py - - - -$ARGUMENTS - -@.base/workspace.json - - - -Follow task: @~/.claude/base-framework/tasks/surface-convert.md - - - -- [ ] .base/data/{name}.json created with migrated items -- [ ] .base/hooks/{name}-hook.py created -- [ ] workspace.json updated with surface registration -- [ ] settings.json updated with hook entry -- [ ] Original markdown file preserved - diff --git a/src/commands/surface-create.md b/src/commands/surface-create.md deleted file mode 100644 index 43cee44..0000000 --- a/src/commands/surface-create.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: base:surface-create -description: Create a new data surface (guided) -argument-hint: "[surface-name]" -allowed-tools: [Read, Write, Edit, Glob, Bash, AskUserQuestion] ---- - - -Create a new data surface through guided conversation. Generates JSON data file, injection hook, workspace.json registration, and settings.json hook entry. - -**When to use:** User wants to track something new as a structured data surface. - - - -@~/.claude/base-framework/tasks/surface-create.md -@.base/hooks/_template.py - - - -$ARGUMENTS - -@.base/workspace.json - - - -Follow task: @~/.claude/base-framework/tasks/surface-create.md - - - -- [ ] .base/data/{name}.json created -- [ ] .base/hooks/{name}-hook.py created -- [ ] workspace.json updated with surface registration -- [ ] settings.json updated with hook entry - diff --git a/src/commands/surface-list.md b/src/commands/surface-list.md deleted file mode 100644 index 39467f7..0000000 --- a/src/commands/surface-list.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: base:surface-list -description: Show all registered data surfaces -allowed-tools: [Read, Bash] ---- - - -Display all registered data surfaces with item counts and hook status. - -**When to use:** User wants to see what surfaces exist. - - - -@~/.claude/base-framework/tasks/surface-list.md - - - -@.base/workspace.json - - - -Follow task: @~/.claude/base-framework/tasks/surface-list.md - - - -- [ ] All registered surfaces displayed with counts - diff --git a/src/commands/weekly-domain.md b/src/commands/weekly-domain.md deleted file mode 100644 index 35d4a39..0000000 --- a/src/commands/weekly-domain.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: base:weekly-domain -description: Create a custom domain phase for the weekly ritual -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] ---- - - -Guided creation of a custom domain phase for /base:weekly. Walks the user through defining what to check, what tools to use, what questions to ask, and what output to produce. - -**When to use:** "Add a domain to my weekly", "create a weekly domain phase", "I want to track X in my weekly". - - - -@~/.claude/base-framework/tasks/weekly-domain-create.md - - - -$ARGUMENTS - -@.base/weekly.json - - - -Follow task: @~/.claude/base-framework/tasks/weekly-domain-create.md - - - -- [ ] User described the domain area -- [ ] Data sources identified (with tool discovery if needed) -- [ ] Weekly questions defined -- [ ] Position in weekly flow chosen -- [ ] Output type specified -- [ ] Domain phase config written to weekly.json - diff --git a/src/commands/weekly.md b/src/commands/weekly.md deleted file mode 100644 index 69b0bb9..0000000 --- a/src/commands/weekly.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: base:weekly -description: Weekly review and planning ritual -allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion] ---- - - -Guided weekly ritual — close the week, plan the next, run maintenance, lock in priorities with calendar events. - -**When to use:** Weekly planning, "run my weekly", "weekly review", "time for my weekly". - - - -@~/.claude/base-framework/tasks/weekly.md -@~/.claude/base-framework/context/base-principles.md - - - -$ARGUMENTS - -@.base/workspace.json -@.base/data/state.json -@.base/weekly.json - - - -Follow task: @~/.claude/base-framework/tasks/weekly.md - - - -- [ ] Week reviewed (daily logs consumed if available) -- [ ] Calendar audited with rules applied -- [ ] Workspace groomed (drift score updated) -- [ ] Priority stack set (outcome-based, aligned to north star) -- [ ] Backlog triaged (overdue items processed) -- [ ] Domain phases executed (if configured) -- [ ] Blockers identified, follow-ups queued -- [ ] Week committed — calendar events created, weekly.json entry logged - diff --git a/src/framework/context/base-principles.md b/src/framework/context/base-principles.md deleted file mode 100644 index ce6e71b..0000000 --- a/src/framework/context/base-principles.md +++ /dev/null @@ -1,69 +0,0 @@ -# BASE Principles - -## Core Laws - -1. **If it's not current, it's harmful.** Stale context documents feed AI bad information. Maintenance isn't optional. -2. **Every file earns its place.** If you can't explain why it's here in 5 seconds, it moves or dies. -3. **Archive > delete.** When in doubt, archive. You can always delete later. You can't un-delete. -4. **The workspace is the product.** Treat it like production code, not a scratch pad. -5. **Clean as you go.** The best time to file something correctly is when you create it. The second best time is now. -6. **Scaffold generates manifest. Manifest drives everything.** One configuration point. No manual bookkeeping. -7. **Tools register themselves.** PAUL projects auto-register with BASE. No human memory required. - -## Drift Score - -Drift is the gap between documented state and actual state. Measured in days-overdue across all tracked areas. - -- **0** — Everything current. Workspace is clean. -- **1-7** — Minor drift. Normal during execution sprints. Fix at next groom. -- **8-14** — Moderate drift. Context documents are likely misleading AI. Groom soon. -- **15+** — Critical drift. Sessions are operating on stale context. Groom NOW. - -## Maintenance Cadence - -| What | Default Cadence | Override | -|------|----------------|---------| -| projects.json | Every session or weekly | workspace.json | -| Project directory | Monthly | workspace.json | -| Tools/MCP | Monthly | workspace.json | -| System layer | Monthly | workspace.json | -| Full audit | Quarterly or after major shifts | On demand | - -## Backlog Rules - -Items have time-based properties enforced by grooming: - -- **Added** — auto-set when item enters backlog -- **Review-by** — priority-based: High=7d, Medium=14d, Low=30d -- **Staleness** — 2x review-by threshold. Auto-archive if reached without action. - -During groom: items past review-by surface as "decide or kill." Items past staleness auto-archive with a note. - -## Graduation Flow - -Backlog items don't sit forever. They graduate to active when the operator is ready to work on them. - -``` -BACKLOG (status=backlog in projects.json) - → ACTIVE (status updated to in_progress/todo via base_update_project) - → DONE (archived via base_archive_project with outcome) -``` - -**TASKS vs PROJECTS:** A task is bounded — it has a finish line. "Extract .mcp.json secrets" is a task. "Build the CARL MCP server" might start as a task but could become a project if it grows. The operator decides during groom. - -**Graduation is never automatic.** The groom flow asks explicitly: "Ready to work on any backlog items?" The operator decides what graduates and where it lands. - -**Items can also move backward:** An active project that loses priority can return to backlog status. A project that stalls can move to DEFERRED. Nothing is permanent. - -## Scaffold Modes - -BASE scaffold operates in two modes: - -- **Standard** (`/base:scaffold`) — Data layer only. Creates `.base/` with workspace.json, `.base/data/state.json`, ROADMAP.md. Scans and tracks what exists. Framework-agnostic. -- **Full** (`/base:scaffold --full`) — Data layer + projects.json + entities.json. Offers CLAUDE.md audit. The "batteries included" version for AI builders who want the full system. - -Standard mode works for any workspace. Full mode provides Chris's proven operational structure. - -## File Location - -BASE operates strictly out of `.base/`. All data (projects.json, entities.json, state.json, psmm.json) lives in `.base/data/`. Configuration (workspace.json) and documentation (ROADMAP.md) live in `.base/`. Data is accessed via MCP tools (`base_list_projects`, `base_add_project`, `base_update_project`, `base_get_state`, etc.). `.base/data/` is the canonical location for all structured data. diff --git a/src/framework/frameworks/audit-strategies.md b/src/framework/frameworks/audit-strategies.md deleted file mode 100644 index 4df45e1..0000000 --- a/src/framework/frameworks/audit-strategies.md +++ /dev/null @@ -1,53 +0,0 @@ -# Audit Strategies - -Reusable audit strategies that can be applied to any workspace area. The workspace manifest (`workspace.json`) maps areas to strategies. The audit command reads the manifest and applies the appropriate strategy to each area. - -## Strategies - -### staleness -**Applies to:** Data files (projects.json, state.json, any tracked document) -**What it does:** Check file modification timestamps against configured thresholds. Flag files past their groom cadence. -**Config:** -- `threshold_days` — days after which the file is considered stale -**Output:** List of stale files with age, recommended action (update or review) - -### classify -**Applies to:** Directories with lifecycle items (projects/, clients/) -**What it does:** List all items in the directory. For each, present to operator for classification: active, archive, or delete. Check for planning docs, recent activity, git history. -**Config:** -- `states` — classification options (default: ["active", "archive", "delete"]) -- `archive_path` — where archived items go (default: `{path}/_archive/`) -**Output:** Classification decisions, items moved to archive, items deleted - -### cross-reference -**Applies to:** Tools/servers that have a config file mapping (e.g., MCP servers vs .mcp.json) -**What it does:** Compare directory contents against a configuration file. Identify directories not referenced in config (orphaned) and config entries pointing to missing directories (broken). -**Config:** -- `config_file` — path to the configuration file to cross-reference -**Output:** Orphaned items, broken references, recommendations - -### dead-code -**Applies to:** System directories (hooks, commands, skills) -**What it does:** Scan for files that appear unused — no references from other files, no recent invocations, no clear purpose. Presents findings for human decision. -**Config:** -- `reference_check` — whether to search for references in other files (default: true) -**Output:** Potentially dead files with evidence, operator decides keep/delete - -### pipeline-status -**Applies to:** Content pipelines, task queues, any workflow with stages -**What it does:** Check items in each pipeline stage. Flag stuck items (in same stage too long), empty stages, bottlenecks. -**Config:** -- `stages` — ordered list of pipeline stages -- `stuck_threshold_days` — days in one stage before flagging -**Output:** Pipeline health report, stuck items, stage distribution - -## Extending Strategies - -Custom strategies can be added for workspace-specific needs. A strategy is defined by: -1. A name (kebab-case) -2. What it applies to (description) -3. What it checks (logic) -4. What config it needs (parameters) -5. What it outputs (findings format) - -Add custom strategies to this file and reference them in `workspace.json`. diff --git a/src/framework/frameworks/claude-config-alignment.md b/src/framework/frameworks/claude-config-alignment.md deleted file mode 100644 index 17eb927..0000000 --- a/src/framework/frameworks/claude-config-alignment.md +++ /dev/null @@ -1,256 +0,0 @@ -# Claude Config Alignment Strategy - -Standalone strategy for auditing `.claude/` directory sprawl across a workspace. Discovers all `.claude/` directories, catalogs their contents, classifies each item against a global/workspace/project hierarchy, and produces a remediation plan. - -Designed to be composed into any audit workflow that touches the system layer. The `/base:audit-claude` command references this strategy directly. The general `/base:audit` can compose it in when running system-layer checks. - ---- - -## When to Use - -- During initial BASE setup on an existing workspace (lots of legacy `.claude/` dirs) -- As part of periodic workspace audits -- After installing a new tool/skill globally and wanting to clean up project-level copies -- When the user suspects their Claude Code config is fragmented - ---- - -## Discovery Rules - -### What to scan -- All directories named `.claude` under the workspace root -- Recursively (projects may nest: `apps/foo/bar/.claude/`) - -### What to skip -- `node_modules/` — third-party packages may contain `.claude/` dirs -- `_archive/` — archived projects are frozen, don't audit -- `.git/` — git internals -- `vendor/`, `dist/`, `build/` — build artifacts - -### What to catalog per directory -For each discovered `.claude/` directory, record: -- **Path** (relative to workspace root) -- **hooks/** — list filenames -- **commands/** — list subdirectories and files -- **skills/** — list skill directories -- **rules/** — list files -- **settings.json** — exists? contents summary -- **settings.local.json** — exists? contents summary -- **Other files** — anything unexpected - ---- - -## Git Boundary Awareness - -Understanding git boundaries is critical for correct classification. The scanner dataset includes `git_boundary` data for each directory. - -### How git boundaries affect visibility - -- **`has_own_git: true`** — This project has its own git root. It does NOT see the workspace root `.claude/` or workspace `.mcp.json`. It only sees global `~/.claude/` + its own `.claude/`. -- **`has_own_git: false`** — This project inherits the workspace root. It sees global `~/.claude/` + workspace root `.claude/` + its own `.claude/`. - -### What this changes about classification - -If a project has its own git root and contains a hook that also exists in the workspace root `.claude/`, that's **not a duplicate running twice** — the workspace root version is invisible to that project. Removing the local copy would leave the project with NO version of that hook. - -For own-git projects, the right framing is: -- Does a current version exist in **global** `~/.claude/`? If yes, the local copy is a true DUPLICATE (global is always visible). -- Does a version only exist in **workspace root** `.claude/`? Then the local copy is the project's ONLY access to that functionality. The right recommendation is PROMOTE_TO_GLOBAL — centralize it so all projects benefit, then clean up local copies. -- Is the local copy an outdated version of something in a baseline? It's DIVERGED. - ---- - -## Classification Rules - -Each item found in a project-level `.claude/` must be classified into exactly one category. These rules define how. - -**Classification order:** TEMPLATE → ACCIDENTAL → DUPLICATE → DIVERGED → PROMOTE_TO_GLOBAL → STALE → GLOBAL_CANDIDATE → PROJECT_SPECIFIC - -### DUPLICATE — Exists in a visible baseline, safe to remove - -An item is a DUPLICATE if: -- Its MD5 hash matches a file in a baseline the project can **actually see** -- Global baseline (`~/.claude/`): always checked — global is always visible -- Workspace root baseline (`.claude/`): only checked if `has_own_git: false` -- A match against a **non-visible** baseline (workspace root for own-git projects) is NOT a duplicate — see PROMOTE_TO_GLOBAL -- Common examples: hooks copied into project dirs that now run globally, skills that were installed globally after being copied locally - -**Verification before removal:** -1. Confirm the global version is the current/active version (not the other way around) -2. Confirm the project's `settings.json` doesn't reference the local copy with a relative path that would break if removed -3. If the local copy has modifications not in the global version, flag as DIVERGED instead - -### DIVERGED — Local copy differs from global - -An item is DIVERGED if: -- A version exists both locally and globally -- The local copy has meaningful differences (not just whitespace or path variations) -- Requires human decision: merge local changes into global? Keep local override? Or discard local changes? - -**Never auto-resolve diverged items. Always present both versions and ask.** - -### PROMOTE_TO_GLOBAL — Centralize to global, then clean up copies - -An item is PROMOTE_TO_GLOBAL if: -- It exists in workspace root `.claude/` (or the same pattern appears across multiple own-git projects) but NOT in global `~/.claude/` -- For own-git projects: a file matching a non-visible baseline (workspace root) is NOT a duplicate — it's a signal something should be centralized -- When multiple projects have the same hook with the same MD5, that's strong evidence it belongs in global -- This is the most valuable finding: "put this in global and stop copying it into every project" - -**PROMOTE_TO_GLOBAL is about removing the need for copies.** Once promoted, all project copies become true DUPLICATES (global is always visible) and can be safely removed. - -**Promotion is a suggestion, never automatic.** After promoting to global, the global `settings.json` must also register the hook or it won't fire. - -### GLOBAL_CANDIDATE — Should be promoted to global (single occurrence) - -An item is a GLOBAL_CANDIDATE if: -- It exists in a project-level `.claude/` but NOT in global -- It serves a user-level purpose (not project-specific) -- It would be useful across multiple projects -- Examples: a custom skill the user installed in one project but uses everywhere, a hook that provides general utility - -**Promotion is a suggestion, never automatic.** Present the item, explain why it's a candidate, let operator decide. - -### PROJECT_SPECIFIC — Legitimately belongs here - -An item is PROJECT_SPECIFIC if: -- It references project-local paths, configs, or conventions -- It only makes sense in the context of this specific project -- Examples: project-specific commands, MCP server lists tailored to that project's stack, hooks that interact with project-local files - -**These stay. Note them for reference but take no action.** - -### STALE — References things that no longer exist - -An item is STALE if: -- settings.json references MCP servers that aren't in the current `.mcp.json` or global config -- hooks reference scripts or tools that have been renamed or removed -- Settings use old configuration patterns that Claude Code no longer supports -- The `.claude/` directory hasn't been modified in 60+ days AND the project itself shows no recent activity - -**Present specific evidence of staleness. Never assume — prove it.** - -### ACCIDENTAL — Clearly unintentional - -An item is ACCIDENTAL if: -- Nested `.claude/.claude/` directories -- Empty `.claude/` directories (no files at all) -- `.claude/` inside directories that aren't projects (temp dirs, scratch folders) - -**Safe to remove, but still confirm with operator.** - -### TEMPLATE — Intentional scaffold template - -An item is TEMPLATE if: -- It lives in a directory named `_template/`, `template/`, or `templates/` -- It contains placeholder values (e.g., `{{PROJECT_NAME}}`) -- It's designed to be copied, not used directly - -**Never modify templates. Note them and move on.** - ---- - -## Settings Reconciliation - -Project-level `settings.json` and `settings.local.json` require special handling because they override global settings. - -### Check for -1. **Hook definitions that duplicate global hooks** — If global `settings.json` already runs `base-pulse-check.py` on UserPromptSubmit, a project that also defines it runs it twice (or runs a stale version) -2. **Stale MCP server references** — Server names change. Old lists reference servers that no longer exist -3. **Empty hook arrays** — `"UserPromptSubmit": []` overrides global hooks with nothing, potentially breaking the user's setup -4. **Permission overrides** — Project-level allow/deny that may conflict with or duplicate global permissions -5. **enabledMcpjsonServers** — Lists that reference old server names - -### Critical safety rule -**An empty hooks array `[]` in a project settings.json OVERRIDES the global hooks with nothing.** This is the most dangerous pattern — it silently disables all hooks for that project. Always flag this explicitly. - ---- - -## Remediation Safety Protocol - -This is the most important section. `.claude/` configuration is what makes Claude Code work. A broken config means a broken development environment. Every remediation action must follow these rules: - -### Before any change -1. **Explain what will change and why** — No "cleaning up your config." Say exactly: "Removing `apps/casegate-v2/.claude/hooks/carl-hook.py` because an identical version runs globally from `~/.claude/hooks/dynamic-rules-loader.py`. The global hook already fires on every prompt in every project." -2. **Show evidence** — Side-by-side comparison, file dates, path references -3. **Wait for explicit approval** — Not "I'll go ahead and clean these up." Ask: "Approve this removal? [y/n]" - -### During changes -4. **One category at a time** — Process all ACCIDENTAL items first (lowest risk), then DUPLICATES, then STALE, then DIVERGED. Save GLOBAL_CANDIDATE promotions for last. -5. **Verify after each change** — After removing a hook, confirm the project's settings.json no longer references it. After removing a skill, confirm no commands reference it. -6. **Never delete settings.json itself** — Even if everything in it is stale. The file's existence may matter. Clean its contents instead, or flag for operator to remove manually. - -### After all changes -7. **Summary report** — What was changed, what was kept, what needs manual follow-up -8. **Recommend a test** — "Open Claude Code in {project} and verify hooks fire correctly" - -### What this workflow NEVER does -- Modify `~/.claude/` (global config) without explicit promotion approval -- Delete a `.claude/` directory entirely (may have gitignore or settings implications) -- Batch-delete without per-item confirmation -- Assume a "messy" config is wrong — it may be working exactly as intended -- Move files between directories (copy + verify + then remove original) - ---- - -## Output Format - -The discovery phase produces an inventory. Present it as: - -``` -## .claude/ Directory Inventory - -Found {N} .claude/ directories ({N} excluding templates and root). - -### {relative/path/.claude/} - hooks/: carl-hook.py, get-current-time-cst.py - settings.json: yes (hooks: 2 UserPromptSubmit) - settings.local.json: yes (MCP servers: 12) - skills/: ui-ux-pro-max/ - commands/: (none) - Last modified: 2026-02-26 - - Classification: - - hooks/carl-hook.py → DUPLICATE (identical to ~/.claude/hooks/dynamic-rules-loader.py) - - hooks/get-current-time-cst.py → DUPLICATE (identical to ~/.claude/hooks/get-current-time-cst.sh) - - settings.json hooks → STALE (references local hook paths that would be removed) - - settings.local.json → PROJECT_SPECIFIC (MCP server list is project-tailored) - - skills/ui-ux-pro-max/ → DUPLICATE (exists at ~/.claude/skills/ui-ux-pro-max/) -``` - -The remediation phase groups by action type and risk level: - -``` -## Remediation Plan - -### Safe Removals (ACCIDENTAL) -1. apps/hunter-exotics/.claude/.claude/ — nested .claude dir (accidental) - Action: Delete entire nested directory - Risk: None - -### Duplicate Removals -2. apps/casegate-v2/.claude/hooks/carl-hook.py — identical to global - Action: Delete file - Risk: Low — must also clean settings.json hook reference - -(... etc, one per item ...) - -### Requires Decision (DIVERGED) -5. apps/hunter-exotics/.claude/settings.local.json — has project-specific MCP list - Local version: [12 servers including project-specific ones] - Global version: [different set] - Recommendation: Keep as PROJECT_SPECIFIC -``` - ---- - -## Composability - -This strategy is a standalone reference document. It does not modify or depend on other strategy files. Any workflow can compose it in: - -- `/base:audit-claude` — reads this strategy directly as its framework -- `/base:audit` — can reference this when auditing the system-layer area -- `/base:groom` — can optionally flag `.claude/` drift during system-layer groom step -- Manual invocation — an operator can ask "audit my .claude dirs" and Claude reads this file - -No registration in workspace.json is required. No modification to audit-strategies.md is needed. diff --git a/src/framework/frameworks/claudemd-strategy.md b/src/framework/frameworks/claudemd-strategy.md deleted file mode 100644 index ca6e363..0000000 --- a/src/framework/frameworks/claudemd-strategy.md +++ /dev/null @@ -1,158 +0,0 @@ -# The CLAUDE.md Strategy - -Composable framework for auditing and writing high-performance CLAUDE.md files. Source of truth for the `/base:audit-claude-md` workflow. - ---- - -## The Structure: What, Why, Who, Where, How - -Every CLAUDE.md follows five sections in this exact order. Each section answers one question. Together they give Claude complete operating context without bloat. - -### What -What this document is and what this workspace contains. - -One line. Sets the contract. Claude knows this is its instruction set. - -> "This file provides guidance to Claude Code when working with code in this repository." - -### Why -The philosophy. Identity context. Why this workspace exists. - -This is where you separate identity from operations. CLAUDE.md answers the "who am I working with?" question. Operational details (how to run a specific project, current sprint status) live elsewhere and get referenced with `@` pointers. - -### Who -Business context. Who the user is, what they do, what matters to them. - -Not a life story. Enough context that Claude can make relevant suggestions. Business name, what the business does, revenue model, team, tech stack. The goal: Claude should be able to answer "what does this person's business look like?" after reading this section. - -### Where -Workspace structure. The directory map. - -This section serves double duty: -1. Tells Claude where to find things -2. Plants the blueprint for the workspace architecture — the Where section describes a structure that may or may not exist yet - -Use a tree diagram. Include: -- Each top-level directory and its purpose -- Key subdirectories if they have meaning -- What goes where (decision guide) - -### How -Ecosystem strategy. Tool strategy. Git strategy. Quick references. - -This is the operational layer: -- What systems/frameworks are in use (compact table with location pointers) -- Git strategy (what gets tracked, what gets ignored) -- Rules (see NEVER pattern below) -- Quick reference table for common actions - ---- - -## The NEVER Pattern: Rules as Anti-Patterns - -**The single most important discovery from 40+ sessions of compliance testing.** - -### The Pattern - -``` -NEVER [wrong action] — [right action] -``` - -Negative framing with absolute language gets near-perfect adherence. Every high-compliance rule follows this format. - -### Why This Works - -It's a binary check. Claude can look at its own output and ask: "Did I do the forbidden thing or not?" No judgment call. No interpretation. No sliding scale. - -| Framing | Compliance | Why | -|---------|-----------|-----| -| "Try to use templates when possible" | ~40% | Ambiguous. "When possible" is a judgment call Claude resolves toward skipping. | -| "Always use templates" | ~65% | Better, but "always" gets weighed against context. Claude may decide exceptions apply. | -| "NEVER create from scratch — use templates" | ~95% | Binary. The forbidden action is unambiguous. | - -### Rules for Writing Rules - -1. **One rule per line.** No compound rules. If it has "and" in it, split it. -2. **The wrong action comes first.** Claude anchors on the first thing it reads. Make the forbidden action the anchor. -3. **The right action is the alternative.** Not a lecture — a redirect. "Don't do X — do Y instead." -4. **Defer complexity to reference docs.** The rule stays short. The details live in a separate file loaded on demand. - ---- - -## The @ Reference System - -CLAUDE.md stays lean by pointing to other files instead of inlining their content. - -``` -@LINKS.md — Personal branding URLs -@projects/dashboard-build/PLANNING.md — Active project context -``` - -Claude reads `@`-referenced files on demand. Your CLAUDE.md stays lean while still giving Claude access to deep context. - -**What to inline vs what to reference:** -- **Inline:** Identity (who, what, why), workspace structure, rules, quick references -- **Reference:** Active project details, task lists, state files, detailed specs - ---- - -## What Stays Out - -The test: **if it changes every week, it doesn't belong in CLAUDE.md.** - -CLAUDE.md is the constitution, not the daily newspaper. - -| Doesn't belong | Where it goes | -|----------------|---------------| -| Task lists / current work | State files, project tracking systems | -| Project specs | Each project's PLANNING.md | -| Rules for specific domains | Domain-specific rule files loaded on demand (e.g., CARL) | -| Daily status updates | State files | -| Detailed framework docs | Separate files, `@`-referenced | - ---- - -## Line Budget - -Target: **under 100 lines.** This is a routing document, not a knowledge base. - -If your CLAUDE.md is over 100 lines, content is being inlined that should be referenced or removed. Common offenders: -- Inline system/framework descriptions (replace with a compact table) -- Redundant location tables when a tree diagram already exists -- Documentation system descriptions (the tree covers this) -- Standalone sections for single rules (consolidate into Rules section) - ---- - -## Audit Criteria - -When auditing an existing CLAUDE.md, check: - -### Structure -- [ ] Follows What → Why → Who → Where → How order -- [ ] Each section is correctly labeled -- [ ] No orphan sections outside the five-section model - -### Content Placement -- [ ] Identity/philosophy in Why (not scattered) -- [ ] Business context in Who (not bloated) -- [ ] Directory map in Where (tree format, not just a table) -- [ ] All operational content in How (git, systems, rules, quick ref) -- [ ] No task lists, state files, or volatile data inlined - -### Rules -- [ ] All rules use NEVER pattern (not "always", "try to", "prefer") -- [ ] One rule per line, no compound rules -- [ ] Wrong action first, right action as redirect -- [ ] Complex rules defer to reference docs - -### Leanness -- [ ] Under 100 lines (excluding code blocks in Where tree) -- [ ] No redundant sections (e.g., key locations table + tree diagram) -- [ ] System descriptions are a compact table, not inline paragraphs -- [ ] `@` references used for anything that changes frequently - -### CARL Integration (if present) -- [ ] Operational rules that belong in domain-specific contexts are flagged for CARL migration -- [ ] CLAUDE.md rules are constitutional (identity-level), not operational -- [ ] No duplication between CLAUDE.md rules and CARL domain rules diff --git a/src/framework/frameworks/satellite-registration.md b/src/framework/frameworks/satellite-registration.md deleted file mode 100644 index 43fcc37..0000000 --- a/src/framework/frameworks/satellite-registration.md +++ /dev/null @@ -1,44 +0,0 @@ -# Satellite Registration Framework - -## What Are Satellites - -Satellites are projects that live in their own git repos inside the workspace (e.g., `apps/*`). They run their own Claude Code sessions independently. BASE needs visibility into them without owning them. - -## Registration Flow - -### Automatic (via PAUL init) -When `/paul:init` runs in a subdirectory: -1. Check if parent directory has `.base/workspace.json` -2. If yes, write registration entry: project name, path, engine type, state file path, date -3. Report: "Registered with BASE workspace: {workspace-name}" - -### Automatic (via BASE scaffold) -When `/base:scaffold` runs: -1. Scan configured satellite directories (default: `apps/`) -2. Detect existing `.paul/` directories -3. Auto-register discovered projects -4. Report: "Found {N} satellite projects. Registered." - -### Automatic (via BASE groom) -During groom: -1. Read registered satellites from workspace.json -2. Check each path exists (clean up broken registrations) -3. Scan satellite directories for unregistered projects with `.paul/` -4. Flag: "Found unregistered project: {name}. Register?" - -## Health Checks - -During `/base:pulse` and `/base:groom`, for each satellite: -1. Read the state file (e.g., `.paul/STATE.md`) -2. Check last modification date -3. Extract current phase/milestone if parseable -4. Report health: active, stale, or unknown - -BASE never modifies satellite state. It only reads and reports. PAUL (or whatever engine) manages the project. BASE manages the workspace those projects live in. - -## Deregistration - -Satellites are deregistered when: -- The project directory no longer exists (auto-cleaned during groom) -- The user explicitly removes it during audit -- The project is archived (moved to `_archive/` or similar) diff --git a/src/framework/tasks/audit-claude-md.md b/src/framework/tasks/audit-claude-md.md deleted file mode 100644 index 9698a63..0000000 --- a/src/framework/tasks/audit-claude-md.md +++ /dev/null @@ -1,171 +0,0 @@ - -Audit an existing CLAUDE.md against the CLAUDE.md Strategy framework, then interactively rewrite it with user approval at each stage. Detects CARL installation and routes operational rules accordingly. - - - -As an AI builder, I want my CLAUDE.md audited against a proven strategy so I get a compliant, lean configuration file — with operational rules properly routed to CARL or preserved as an artifact for later. - - - -- During /base:scaffold (optional step) -- When user says "audit my claude.md", "improve my claude.md", "rewrite my claude.md" -- Entry point: /base:audit-claude-md - - - -@~/.claude/base-framework/frameworks/claudemd-strategy.md -@~/.claude/base-framework/templates/claudemd-template.md - - - - - -Load the CLAUDE.md Strategy framework and template. - -1. Read `@~/.claude/base-framework/frameworks/claudemd-strategy.md` — this is the source of truth -2. Read `@~/.claude/base-framework/templates/claudemd-template.md` — this is the structural reference -3. Internalize: five-section model (What/Why/Who/Where/How), NEVER pattern, line budget, audit criteria - -You MUST understand the full strategy before reading the user's file. The strategy defines what "correct" looks like. - - - -Read the user's existing CLAUDE.md and catalog every piece of content. - -1. Read `CLAUDE.md` from workspace root -2. If no CLAUDE.md exists → skip to `generate_fresh` step -3. For every section, paragraph, rule, table, and reference in the file, classify each as: - - **KEEP** — belongs in CLAUDE.md per the strategy (identity, structure, constitutional rules) - - **REMOVE** — doesn't belong (volatile data, task lists, state references, redundant sections) - - **RESTRUCTURE** — right content, wrong location or format (e.g., rule using "always" instead of NEVER pattern, operational content in wrong section) - - **CARL_CANDIDATE** — operational rule or domain-specific behavior that belongs in a rules engine, not CLAUDE.md - -4. Count total lines. Note if over 100-line budget. - - - -Present the full audit to the user. This is INTERACTIVE — do not proceed without approval. - -Present a structured report: - -**Section Order Compliance:** -- Current order vs required order (What → Why → Who → Where → How) -- Orphan sections (content outside the five-section model) - -**Content Classification:** -For each piece of existing content, show: -``` -[KEEP] "Business context section" → stays in Who -[REMOVE] "Active Work section" → volatile, belongs in state management -[RESTRUCTURE] "LSP rule" → move to How/Rules, convert to NEVER pattern -[CARL_CANDIDATE] "When writing tests, always..." → operational rule, not identity -``` - -**Line Budget:** -- Current: {N} lines -- Target: under 100 -- Reduction plan: what removal/restructuring achieves - -**Missing Content:** -- Sections required by strategy that don't exist yet - -Ask: **"Does this audit look right? Any items you want to reclassify before I proceed?"** - -Wait for user response. Adjust classifications based on their feedback. - - - -Check for CARL installation to determine rule routing. - -1. Check for `.carl/manifest` in workspace root (workspace-level CARL) -2. Check for `~/.carl/manifest` (global-level CARL) -3. If CARL found: - - Report: "CARL detected at {location}. Operational rules will be proposed as CARL domain rules." - - Note which existing CARL domains overlap with CARL_CANDIDATE items -4. If CARL not found: - - Report: "No CARL installation detected." - - Offer: "I can: (a) install CARL now and set up domains, or (b) save operational rules as an artifact in `.base/artifacts/` for later CARL setup" - - If user picks (b): rules go to `.base/artifacts/claudemd-audit-rules.md` (create `.base/artifacts/` if needed) - - If user picks (a): guide CARL installation, then route rules to domains - -Wait for user decision before proceeding. - - - -Build the new CLAUDE.md section by section, presenting each for approval. - -For EACH section (What, Why, Who, Where, How): - -1. **Show the proposed content** for that section -2. **Show what changed** vs the original (additions, removals, restructuring) -3. **Ask for approval**: "Accept this section? Or modify?" -4. If user modifies → incorporate changes -5. If user accepts → lock section, move to next - -**Section-specific guidance:** - -**What:** One-liner. Rarely needs changes unless missing entirely. - -**Why:** Philosophy/identity. Pull from existing philosophy content. Strip operational routing details that belong in How. - -**Who:** Business context. Preserve existing content. Trim if bloated. Ensure it answers "what does this person's business look like?" - -**Where:** Scan actual filesystem with `ls` to verify tree accuracy. Update tree to match reality. Remove subdirectories that don't add meaning. Collapse verbose trees to stay within budget. - -**How:** Assemble from: -- Systems table (compact, one row per system) -- Git strategy table (from existing or detected .gitignore) -- Rules (NEVER pattern only — constitutional rules stay here, operational rules route to CARL/artifact) -- Quick reference (common actions → instructions) - - - -Handle operational rules that were classified as CARL_CANDIDATE. - -**If CARL is installed:** -1. Group candidates by likely CARL domain (DEVELOPMENT, CONTENT, CLIENTS, etc.) -2. Present: "These rules are proposed for CARL domain `{domain}`:" -3. Show each rule in NEVER pattern format -4. Ask: "Approve these for CARL? Modify? Skip?" -5. For approved rules: provide the exact CARL command to add them (do NOT execute without explicit permission) - -**If CARL artifact path:** -1. Write all CARL_CANDIDATE rules to `.base/artifacts/claudemd-audit-rules.md` -2. Format: group by proposed domain, NEVER pattern, include rationale -3. Tell user: "Rules saved to `.base/artifacts/claudemd-audit-rules.md`. When you're ready to set up CARL, run this file through Claude to create the domains." - - - -Write the approved CLAUDE.md. - -1. Assemble all approved sections into final document -2. Verify line count (warn if over 100) -3. Write to `CLAUDE.base.md` in workspace root (NEVER overwrite CLAUDE.md directly) -4. Present final diff summary: sections added, removed, restructured, rules routed - -Tell user: -- "Review `CLAUDE.base.md`. To adopt it: `mv CLAUDE.base.md CLAUDE.md`" -- "Your original CLAUDE.md is untouched." -- If CARL candidates were routed: "Operational rules are in {location}." - - - - - -- `CLAUDE.base.md` — strategy-compliant CLAUDE.md ready for adoption -- CARL domain rules (if CARL installed) or `.base/artifacts/claudemd-audit-rules.md` (if not) -- Original CLAUDE.md untouched - - - -- [ ] Strategy framework loaded and understood before audit begins -- [ ] Every line of existing CLAUDE.md classified (KEEP/REMOVE/RESTRUCTURE/CARL_CANDIDATE) -- [ ] Full audit presented to user with approval gate before rewriting -- [ ] CARL installation detected and rule routing decided with user -- [ ] Each section proposed individually with user approval -- [ ] All rules use NEVER pattern -- [ ] Final output under 100 lines -- [ ] Operational rules routed to CARL or saved as artifact -- [ ] Original CLAUDE.md never modified -- [ ] User informed of how to adopt and next steps - diff --git a/src/framework/tasks/audit-claude.md b/src/framework/tasks/audit-claude.md deleted file mode 100644 index 9015ef0..0000000 --- a/src/framework/tasks/audit-claude.md +++ /dev/null @@ -1,330 +0,0 @@ - -Audit all .claude/ directories across a workspace. Discover sprawl, classify each item against the global/workspace/project hierarchy, plan remediation with operator approval at every step, and execute changes safely. - - - -As an AI builder with multiple projects in my workspace, I want all my .claude/ directories audited for duplication, staleness, and misplacement, so that my Claude Code configuration is clean, consistent, and I know exactly what's project-specific vs what should be global. - - - -- During BASE setup on an existing workspace with legacy projects -- Periodically as part of workspace optimization -- After installing global skills/hooks and wanting to clean up project copies -- When user says "audit my claude config", "clean up my .claude dirs", "check claude setup" -- Entry point routes here via /base:audit-claude - - - -@frameworks/claude-config-alignment.md - - - -ALL audit findings MUST be written to a markdown report file at `.base/audits/claude-config-{YYYY-MM-DD}.md`. - -Do NOT dump findings into the chat as inline text. The chat is for brief status updates, questions, and confirmations only. The report is where all detail lives. - -The report must be: -- Written in clean markdown with tables, headers, and clear visual hierarchy -- Readable by a human who opens it in any markdown viewer -- Comprehensive: current state, classifications with evidence, remediation plan with risk levels, items kept and why - -After writing the report, tell the operator: "Audit report written to `.base/audits/claude-config-{date}.md`. Review it, then tell me which remediation groups to execute." - -During remediation execution, update the report with results (append a "Remediation Results" section). - - - - - -Run the scanner utility to produce a complete, verified dataset. - -This step uses a deterministic Python script that scans the entire workspace and produces structured JSON. The script handles all data collection — baselines, directory discovery, file hashing, settings parsing. Claude does NOT gather this data manually. - -**Run the scanner:** -``` -python3 ~/.claude/base-framework/utils/scan-claude-dirs.py --workspace {workspace_root} -``` - -The script outputs a JSON file to `.base/audits/data-sets/claude-scan-{date}.json` containing: -- **Baselines:** Complete inventory of global `~/.claude/` and workspace root `.claude/` (every file with MD5 hash) -- **MCP registry:** All server names from `.mcp.json` -- **Directories:** Every project-level `.claude/` directory with full contents (hooks, commands, skills, rules, settings — all with MD5 hashes) -- **Summary:** Counts of hooks, commands, skills, settings files, nested dirs, empty dirs, templates - -**Read the JSON output.** This is your single source of truth for all subsequent steps. Do not make ad-hoc bash calls to re-discover or re-hash files. If it's not in the scan data, re-run the scanner. - -If the scanner fails, diagnose and fix before proceeding. Do not fall back to manual scanning. - - - -Classify every item in every project-level .claude/ directory. - -**CRITICAL: Git boundary awareness.** - -The scanner dataset includes `git_boundary` data for each directory. This tells you what each project actually sees when Claude Code boots there: - -- `has_own_git: true` → This project has its own git root. It does NOT see the workspace root `.claude/` or workspace `.mcp.json`. It only sees global `~/.claude/` + its own `.claude/`. -- `has_own_git: false` → This project inherits the workspace root. It sees global `~/.claude/` + workspace root `.claude/` + its own `.claude/`. - -**This changes what "duplicate" means.** If a project has its own git root and contains a hook that also exists in the workspace root `.claude/`, that's not a duplicate running twice — the workspace root version is invisible to that project. Removing the local copy would leave the project with NO version of that hook. - -For own-git projects, the right framing is: -- Does a current version exist in **global** `~/.claude/`? If yes, the local copy is a true duplicate (global is always visible). -- Does a version only exist in **workspace root** `.claude/`? Then the local copy is the project's ONLY access to that functionality. The right recommendation is to **promote to global** so all projects benefit, THEN clean up local copies. -- Is the local copy an outdated version of something in a baseline? It's DIVERGED — recommend updating to current or promoting current to global. - -**Classification order:** - -1. **TEMPLATE** — In a template directory? Mark and skip. -2. **ACCIDENTAL** — Nested .claude dirs, empty dirs. -3. **DUPLICATE** — MD5 matches a baseline the project can ACTUALLY SEE: - - Global baseline: always checked (always visible) - - Workspace root baseline: only checked if `has_own_git: false` - - Match against visible baseline → DUPLICATE (safe to remove) - - Match against non-visible baseline → NOT a duplicate. See PROMOTE_TO_GLOBAL. -4. **DIVERGED** — Same-named file exists in a visible baseline but MD5 differs. -5. **PROMOTE_TO_GLOBAL** — Item exists in workspace root `.claude/` (or same pattern across multiple projects) but NOT in global `~/.claude/`. These are hooks/skills/commands the user wants everywhere but hasn't centralized. This is the most valuable finding — "put this in global and stop copying it into every project." When multiple projects have the same hook, that's strong evidence. -6. **STALE** — References things that no longer exist: - - settings.json hooks pointing to missing files - - settings.local.json MCP servers not in registry (also check if MCP is even visible per git boundary) - - Files untouched for 60+ days in an inactive project -7. **GLOBAL_CANDIDATE** — Exists only in one project, serves a user-level purpose, not in any baseline. -8. **PROJECT_SPECIFIC** — Everything else that legitimately belongs in the project. - -**Classification rules:** -- A file is DUPLICATE only if its MD5 matches a baseline the project can actually see -- A file matching a non-visible baseline is a signal that something should be promoted to global -- PROMOTE_TO_GLOBAL is the key recommendation for own-git projects with hooks/skills that mirror workspace root -- When multiple projects share the same hook, that's strong evidence it belongs in global -- When in doubt, check the hash (hashes don't lie) and the git_boundary data (assumptions lie) - - - -Analyze settings.json and settings.local.json files specifically. - -These are the most dangerous files because they control Claude Code behavior: - -1. For each project-level settings.json: - a. Parse hook definitions — list every hook command - b. For each hook, check: does this reference a local file? Does that file exist? Is it a duplicate? - c. Check if hook arrays are empty `[]` — this OVERRIDES global hooks with nothing - d. Compare hook list against global settings.json hooks — identify double-execution patterns - e. Check permissions — do they conflict with or duplicate global? - -2. For each project-level settings.local.json: - a. List every entry in enabledMcpjsonServers - b. Check each server name against MCP baseline — flag any that don't exist - c. Note enableAllProjectMcpServers boolean - -3. **Git-aware analysis** — For each project, use git_boundary data to determine: - - Which hooks are ACTUALLY running (global + project? or global + workspace root + project?) - - Is the project's `.mcp.json` visibility correct? (own-git projects can't see workspace .mcp.json) - - Would removing a local hook leave the project with NO version of that functionality? - -4. Build a "Settings Danger Report" section: - - Which projects have duplicate hooks running (only possible if project inherits workspace root) - - Which own-git projects rely on local hooks as their ONLY source of CARL/time/etc functionality - - Which settings.json files will have dangling references after hook files are removed - - Which settings.local.json files have stale MCP entries (or reference MCP servers they can't even see) - - - -Self-audit: verify every classification is correct before writing the report. - -This step exists because classification errors are the most damaging mistake this audit can make. A misclassified DUPLICATE that's actually PROJECT_SPECIFIC means deleting something the user needs. - -**Verification checks:** - -1. **DUPLICATE verification** — For every item classified as DUPLICATE: - - Confirm the baseline file it supposedly duplicates actually exists - - Confirm the MD5 hashes actually match (re-check, don't trust prior step) - - Confirm the baseline version is the current/active version - -2. **GLOBAL_CANDIDATE verification** — For every item classified as GLOBAL_CANDIDATE: - - Search global baseline for any file with the same name (case-insensitive) - - Search workspace root baseline for any file with the same name - - Search for the same MD5 hash across all baselines (catches renamed copies) - - If found anywhere → reclassify as DUPLICATE - -3. **DIVERGED verification** — For every item classified as DIVERGED: - - Confirm both versions actually exist and differ - - Note which is newer (by file modification date) - -4. **STALE verification** — For every STALE settings entry: - - Confirm the referenced resource actually doesn't exist (not just renamed) - - Check both .mcp.json AND global settings for MCP servers - -5. **Completeness check** — Count total items classified vs total items discovered. If they don't match, something was missed. Find and classify the missing items. - -6. **Cross-reference check** — For items in the remediation plan that depend on each other (e.g., deleting a hook file + cleaning the settings.json that references it), verify both sides of the dependency are in the plan. - -If any reclassifications happen in this step, update all downstream plan entries. - - - -Write the complete audit report to `.base/audits/claude-config-{YYYY-MM-DD}.md`. - -Report structure: -1. **MD5 disclaimer** — Always include this at the top, right after the metadata block, as a blockquote: - > *\* This audit uses MD5 fingerprinting to classify files. An MD5 hash is a unique fingerprint generated from a file's contents — if two files produce the same fingerprint, they are byte-for-byte identical. If the fingerprints differ, the files are different, even if they share the same name. This means every "DUPLICATE" classification in this report is provably exact, and every "DIVERGED" classification is provably different — not guessed, not assumed.* -2. **Summary** — What's wrong, what's fine, item counts by classification -2. **Baselines** — Brief description of what exists globally and at workspace root (so the reader understands what "duplicate of" means) -3. **Findings by Directory** — Each project .claude/ gets its own section with: - - Table of items: item path, classification, evidence (MD5 match, baseline reference) - - Last modified date - - Risk notes specific to that directory -4. **Settings Reconciliation** — Dangerous patterns: duplicate hook execution, stale MCP refs, empty hook overrides -5. **Remediation Plan** — Grouped by risk level (1-6), each item has: - - Item number - - Exact path - - Action (delete, clean, keep, promote) - - Why (with specific evidence) - - Dependencies (e.g., "after removing hook file, settings.json needs cleanup in Group 4") -6. **Items Kept (No Action)** — What's staying and exactly why -7. **Next Steps** — What the operator should do - -Tell the operator: "Audit report written to `.base/audits/claude-config-{date}.md`. Review it, then we'll decide how to handle remediation." - -**Wait for operator to review the report before proceeding to graduation routing.** - - - -Route remediation into a structured execution path. Present the operator with options. - -**Display routing prompt:** -``` -════════════════════════════════════════ -AUDIT COMPLETE — REMEDIATION ROUTING -════════════════════════════════════════ - -The audit report is ready at .base/audits/claude-config-{date}.md - -How would you like to handle remediation? - -[1] Create standalone PAUL project - → Initializes a new PAUL project seeded with audit findings - → Best for: large remediation, multiple phases, traceability needed - -[2] Add milestone to existing PAUL project - → Adds a remediation milestone to a registered satellite - → Best for: audit is part of ongoing workspace optimization work - -[3] Execute ad-hoc (legacy) - → Proceed with group-by-group remediation in this session - → Best for: small, straightforward cleanups - -════════════════════════════════════════ -``` - -**If option 1 (standalone PAUL project):** - -1. Ask: "Where should the project be created? (e.g., `projects/claude-audit-remediation`)" -2. If user has no obvious location, suggest `projects/` as a convention -3. Provide instructions: - ``` - To proceed: - 1. mkdir -p {path} - 2. cd {path} - 3. Run /paul:init - 4. When defining scope, reference the audit report: - @.base/audits/claude-config-{date}.md - - The audit report's Remediation Plan section maps directly - to PAUL phases — each remediation group can be a phase. - ``` -4. Do NOT auto-run /paul:init — the operator invokes it in the right context -5. Exit this workflow (remediation happens through PAUL) - -**If option 2 (add milestone to existing PAUL project):** - -1. Read `.base/workspace.json` for registered satellites: - ``` - Read workspace.json → satellites array → list projects with paths and current status - ``` -2. Present registered projects: - ``` - Registered PAUL projects: - [a] apps/base — v2.4 Config Governance (in progress) - [b] apps/casegate-v2 — v1.0 (if registered) - ... - - Select a project, or provide a path: - ``` -3. After selection, provide instructions: - ``` - To proceed: - 1. In the selected project, run /paul:milestone - 2. When defining scope, reference the audit report: - @.base/audits/claude-config-{date}.md - - The audit report's Remediation Plan section provides - the scope — each remediation group maps to a phase. - ``` -4. Do NOT auto-run /paul:milestone -5. Exit this workflow (remediation happens through PAUL) - -**If option 3 (ad-hoc / legacy):** - -Proceed directly to the execute_remediation step below. This preserves the original workflow behavior for operators who prefer immediate execution. - -``` -Proceeding with ad-hoc remediation. -Tell me which groups to execute (or "approve all"). -``` - - - -Execute approved remediation items one group at a time. - -For each approved group: -1. Announce in chat: "Executing Group {N}: {description} ({count} items)" -2. For each item: - a. Execute the change - b. If the change involves a settings.json modification, verify the JSON is still valid after edit - c. Brief confirmation in chat: "{path} — done" -3. After each group completes: - a. Verify no broken references were created - b. Report in chat: "Group {N} complete. {count} items processed." -4. If any item fails or produces an unexpected result, STOP and report to operator - -**Between groups, pause and confirm: "Group {N} complete. Proceed to Group {N+1}?"** - - - -Verify the workspace is healthy after all remediation and update the report. - -1. Re-scan every `.claude/` directory that was modified -2. For each modified directory verify: - - settings.json is valid JSON (if it was modified) - - No hook arrays reference files that don't exist - - No empty `.claude/` directories left behind (unless intentional) - - No orphaned subdirectories (hooks/ dir with no hooks in it) -3. Run a quick re-discovery scan to catch anything the remediation might have exposed -4. Append a "Remediation Results" section to the audit report file: - - What was executed (by group) - - What was verified - - Any issues found during verification - - Projects the operator should test by opening Claude Code in them - -Tell the operator: "Remediation complete. Report updated. Recommend testing Claude Code in: {list of modified projects}." - - - - - -Complete .claude/ directory audit with inventory, classification, verified remediation, and post-remediation verification — all in a structured markdown report. - - - -- [ ] Three baselines built (global, workspace root, MCP registry) before any classification -- [ ] Every file hashed with MD5 — no classification without hash evidence -- [ ] Every item classified against ALL baselines, not just one -- [ ] Self-audit pass completed — all classifications verified, no GLOBAL_CANDIDATE that's actually a DUPLICATE -- [ ] Item count verified: classified items == discovered items (nothing missed) -- [ ] Settings files analyzed for dangerous patterns (empty hooks, stale MCP refs, double execution) -- [ ] Report written to .base/audits/ as structured markdown (not inline chat) -- [ ] Operator reviewed report and approved remediation before execution -- [ ] Changes executed one group at a time with verification between groups -- [ ] Post-remediation scan confirms no broken references or invalid JSON -- [ ] Report updated with remediation results - diff --git a/src/framework/tasks/audit.md b/src/framework/tasks/audit.md deleted file mode 100644 index 27ac7df..0000000 --- a/src/framework/tasks/audit.md +++ /dev/null @@ -1,64 +0,0 @@ - -Deep workspace optimization. Dynamically generate audit phases from the workspace manifest, run each area's configured audit strategy, and execute operator-approved changes. - - - -As an AI builder, I want a thorough workspace audit that adapts to my workspace structure, so that every area gets properly reviewed regardless of how complex my setup is. - - - -- Quarterly or after major workspace shifts -- When user says "base audit", "deep clean", "optimize workspace" -- Entry point routes here via /base:audit - - - - - -Read workspace manifest and generate audit phases dynamically. - -1. Read `.base/workspace.json` -2. For each area, create an audit phase using its configured strategy -3. Present phase list: "Audit will cover {N} phases: {list with strategies}" -4. Create task tracking for each phase - -**Wait for operator confirmation. Allow them to skip or reorder phases.** - - - -Run each phase using its configured audit strategy. - -For each phase: -1. Announce: "Phase {N}: {area-name} ({strategy})" -2. Execute the strategy (reference frameworks/audit-strategies.md) -3. Present findings -4. Collect operator decisions (keep/archive/delete/move) -5. Execute approved changes -6. Mark phase complete - -Strategies are documented in `@frameworks/audit-strategies.md`. - - - -Record the audit results. - -1. Update `.base/data/state.json` -2. Write audit record to `.base/audits/{YYYY-MM-DD}.md` -3. Log to `.base/ROADMAP.md` -4. Report final summary: phases completed, items changed, new drift score - - - - - -Complete workspace audit with dynamic phases. All areas reviewed, changes executed, audit recorded. - - - -- [ ] Phases generated dynamically from manifest (not hardcoded) -- [ ] Each area audited using its configured strategy -- [ ] Operator approved all changes before execution -- [ ] Audit record written to audits/ directory -- [ ] state.json updated -- [ ] ROADMAP.md updated with audit entry - diff --git a/src/framework/tasks/carl-hygiene.md b/src/framework/tasks/carl-hygiene.md deleted file mode 100644 index a7864f7..0000000 --- a/src/framework/tasks/carl-hygiene.md +++ /dev/null @@ -1,142 +0,0 @@ - -Structured CARL domain maintenance. Review staged proposals, flag stale rules, audit domain health, and keep CARL lean and accurate. All operations target carl.json as the single source of truth. - - - -As an AI builder, I want a guided CARL maintenance session, so that my domain rules stay relevant, staged proposals get decided on, and CARL doesn't become a dumping ground of stale rules. - - - -- Monthly (on configured cadence) -- When pulse reports overdue CARL hygiene -- When user says "carl hygiene", "review carl rules", "clean up carl" -- Entry point routes here via /base:carl-hygiene - - - - - -Gather CARL health data and present summary. - -1. Read `.base/workspace.json` for `carl_hygiene` config (threshold, max rules, last run) -2. Use `carl_v2_list_domains` to get all domains with rule/decision counts and state -3. Use `carl_v2_get_staged` to check for pending staging proposals -4. For each active domain from `carl_v2_list_domains`: - - Note rule count and decision count - - Use `carl_v2_get_domain(domain)` to inspect rule `last_reviewed` fields - - Flag rules where `last_reviewed` is null or older than `staleness_threshold_days` - - Flag domains exceeding `max_rules_per_domain` -5. Present summary: - ``` - CARL Hygiene Assessment - ━━━━━━━━━━━━━━━━━━━━━━ - Staged proposals: {N} pending - Domains: {N} total ({N} active, {N} inactive), {N} with stale rules, {N} over max - Total rules: {N} across all domains - Total decisions: {N} ({N} active, {N} archived) - Last hygiene: {date or "never"} - ``` - -**Wait for operator confirmation before proceeding.** - - - -Process each pending staged proposal. - -Use `carl_v2_get_staged` to retrieve all proposals. For each with `status: "pending"`: -1. Present: - ``` - Proposal {id} — {proposed_domain} - Proposed: {created_at} | Source: {source_session or "manual"} - Rule: "{rule_text}" - Rationale: {rationale} - ``` -2. Ask: "**Approve**, **Kill**, or **Defer**?" -3. Execute: - - Approve → `carl_v2_approve_proposal(id)` — promotes to domain rule with `source: "staging"`, removes from staging - - Kill → Read `.carl/carl.json`, remove the proposal entry from the `staging` array, write back - - Defer → skip (stays pending for next hygiene) - -If no pending proposals: "No staged proposals to review." and move to next step. - -Process one proposal at a time. Wait for response between each. - - - -Review rules flagged as stale (last_reviewed is null or older than threshold). - -For each domain with stale rules (identified in assess step): -1. Present domain name and total rule count -2. For each stale rule: - ``` - [{DOMAIN}] Rule {id} — last reviewed {date or "never"} ({days} days ago) - "{text}" - ``` -3. Ask: "**Keep** (update reviewed date), or **Kill**?" -4. Execute: - - Keep → Use `carl_v2_replace_rules(domain, rules)` with updated `last_reviewed` set to today's date for kept rules - - Kill → Use `carl_v2_remove_rule(domain, rule_id)` (with "Are you sure?" confirmation) - -If no stale rules: "All rules are current. No staleness issues." and move to next step. - -Process one domain at a time. - - - -Quick domain health check — guided Q&A. - -1. List all domains (active and inactive) with rule counts from `carl_v2_list_domains` -2. For each active domain: - - "Do the recall phrases for **{domain}** still match how you talk about this work?" - - Show current recall keywords for reference -3. Check for domains over `max_rules_per_domain`: - - "Domain **{X}** has {N} rules (max: {max}). Any candidates to kill or consolidate?" -4. Check for inactive domains: "These domains are inactive: {list}. Reactivate or remove any?" -5. Ask: "Any new domains to create? Any to deactivate?" - -**Guided Q&A — don't force changes, just surface questions.** - - - -Quick check on per-domain decision health. - -For each domain that has decisions (from `carl_v2_get_domain`): -1. List decisions with date and status -2. Flag decisions older than 90 days (might be outdated) -3. Flag domains with 0 decisions that might benefit from decision logging -4. Ask: "Any decisions to archive?" → use `carl_v2_archive_decision(id)` if yes - -**Brief pass — decisions are mostly self-maintaining.** - - - -Record the hygiene session. - -1. Update `.base/workspace.json` → `carl_hygiene.last_run` to today's date -2. Update `.base/data/state.json` → note CARL hygiene completed with timestamp -3. Report: - ``` - CARL Hygiene Complete - ━━━━━━━━━━━━━━━━━━━━━ - Proposals: {N} processed ({N} approved, {N} killed, {N} deferred) - Rules reviewed: {N} ({N} kept, {N} killed) - Decisions reviewed: {N} ({N} archived) - Domains: {N} active, {N} inactive - Next hygiene due: {date based on cadence} - ``` - - - - - -CARL domains reviewed and maintained. Staged proposals decided. Stale rules addressed. Domain health verified. Hygiene session logged to workspace.json. - - - -- [ ] All pending proposals presented and decided (approve/kill/defer) -- [ ] Stale rules flagged and reviewed with operator -- [ ] Domain health check completed (rule counts, recall phrases) -- [ ] workspace.json carl_hygiene.last_run updated -- [ ] state.json updated with hygiene completion -- [ ] Operator confirmed completion of each step - diff --git a/src/framework/tasks/groom.md b/src/framework/tasks/groom.md deleted file mode 100644 index b343a98..0000000 --- a/src/framework/tasks/groom.md +++ /dev/null @@ -1,157 +0,0 @@ - -Structured weekly maintenance cycle. Walk through each workspace area, review with operator, enforce backlog time-based rules, graduate ready items, and log the groom. - - - -As an AI builder, I want a guided workspace maintenance session, so that my context documents stay current, my backlog items graduate when ready, and my workspace doesn't drift. - - - -- Weekly (on configured groom day) -- When pulse reports overdue grooming -- When user says "base groom", "let's groom", "workspace maintenance" -- Entry point routes here via /base:groom - - - - - -Determine what needs grooming. - -1. Read `.base/workspace.json` manifest -2. Use `base_get_state` MCP tool (or read `.base/data/state.json`) for last groom dates per area -3. Identify which areas are due for grooming (past their cadence) -4. Sort by staleness (most overdue first) -5. Present: "Groom session starting. {N} areas due for review: {list}. Estimated time: {N*5} minutes." - -**Wait for operator confirmation before proceeding.** - - - -Review projects — the working memory for all active, blocked, and backlog work. - -**Data source:** `base_list_projects` MCP tool (reads projects.json) - -1. Use `base_list_projects` to pull all projects grouped by status -2. Present summary: "{N} active, {N} blocked, {N} backlog, last updated {date}" -3. For each active/blocked project: "Still active? Status changed? Next action current?" -4. For each task (type=task): "Done? Still in progress? Blocked?" -5. Archive completed items via `base_archive_project` -6. Ask: "Anything new to add?" -7. Updates via `base_update_project` - -**Backlog items (status=backlog) — enforce time-based rules:** -1. For each backlog item, check `created_at` or `review_by` against thresholds: - - High priority: 7 days - - Medium priority: 14 days - - Low priority: 30 days -2. Items past review-by → surface: "These items need a decision: {list}" -3. Items past staleness (2x review-by) → "Auto-archiving: {list} (past {N} days without action)" -4. Process operator decisions on each flagged item - -**Graduation check:** -5. For each remaining backlog item, ask: "Ready to work on any of these?" -6. If yes — update status from `backlog` to `in_progress` or `todo` via `base_update_project` -7. If no — keep with updated review-by date - -**The graduation question is explicit every groom.** Items don't graduate silently — the operator decides. - -Voice-friendly: walk through one entry at a time, wait for response. - - - -Review directory-type areas (projects/, clients/, tools/). - -For each directory area due for grooming: -1. List contents -2. Flag anything that looks orphaned or new since last groom -3. Ask: "Anything to archive, delete, or reclassify?" -4. Execute approved changes - - - -Review PAUL satellite project health. - -1. Read `.base/workspace.json` — collect all satellite entries where `groom_check: true` -2. If no satellites have `groom_check: true` → skip this step silently -3. For each eligible satellite: - a. Read its STATE.md at the path in `satellite.state` (relative to workspace root) - b. If STATE.md is missing or unreadable → note as "⚠️ {name}: STATE.md not found" - c. Get last activity timestamp: - - PRIMARY: read `satellite.last_activity` from workspace.json entry (ISO timestamp written by session-start hook from paul.json) - - FALLBACK: if `last_activity` not present in workspace.json, parse "Last activity" line from the satellite's STATE.md - - If neither available → note as "⚠️ {name}: cannot determine last activity" - d. Parse "Loop Position" section from STATE.md → extract PLAN/APPLY/UNIFY markers (✓ = done, ○ = pending) - e. Evaluate health criteria: - - **STUCK LOOP**: Loop shows PLAN ✓ APPLY ○ or PLAN ✓ APPLY ✓ UNIFY ○, AND last activity > 7 days ago - - **ABANDONED PHASE**: Last activity > 14 days ago AND milestone status is not COMPLETE - - **MILESTONE DRIFT**: Milestone marked COMPLETE, loop shows ○ ○ ○ (no new milestone started), AND last activity > 14 days ago -4. Collect all issues across satellites -5. If issues found: surface as: - ``` - ⚠️ Satellite health issues: - - {satellite-name}: {issue type} (last active: {date}) - ``` -6. If no issues: output single line "Satellites: all healthy ({N} checked)" - -**Report only — do NOT auto-fix.** Operator decides what to do with flagged satellites. - - - -Review system layer areas (hooks, commands, skills, CARL). - -1. Quick scan for obvious dead items -2. Only flag if something clearly wrong -3. Ask: "Any system changes to note?" -4. If CARL hygiene is enabled (workspace.json `carl_hygiene.proactive: true`): - - Use `carl_v2_get_staged` to check for pending proposals in carl.json - - Use `carl_v2_list_domains` to check rule counts and spot-check `last_reviewed` dates for staleness - - Surface: "{N} staged proposals, {N} stale rules — run /base:carl-hygiene?" - - - -Record the groom session. - -1. Use `base_record_groom` MCP tool to update state.json (sets last_groom, advances next_groom_due) -2. Use `base_update_drift` MCP tool to reset drift indicators -3. Update area timestamps via `base_update_area` for each groomed area -4. Write groom summary to `.base/grooming/{YYYY}-W{NN}.md`: - ```markdown - # Groom Summary — Week {NN}, {YYYY} - - **Date:** {YYYY-MM-DD} - **Areas Reviewed:** {list} - **Drift Score:** {before} → 0 - - ## Changes - - {what changed} - - ## Graduated from Backlog - - {item} → project (status: in_progress) - - ## Archived / Killed - - {item} (reason) - - ## Next Groom Due - {YYYY-MM-DD} - ``` -5. Report: "Groom complete. Drift score: 0. Next groom due: {date}." - - - - - -Updated workspace state. All due areas reviewed and current. Backlog time-based rules enforced. Ready items graduated. Groom summary logged. - - - -- [ ] All overdue areas reviewed with operator -- [ ] Projects updated via base_update_project / base_archive_project -- [ ] Backlog time-based rules enforced (review-by, staleness) -- [ ] Graduation question asked explicitly for backlog items -- [ ] Graduated items updated from backlog → active status -- [ ] state.json updated via base_record_groom -- [ ] Groom summary written to grooming/ directory -- [ ] Drift score reset to 0 -- [ ] Operator confirmed completion of each area - diff --git a/src/framework/tasks/history.md b/src/framework/tasks/history.md deleted file mode 100644 index 67a20f9..0000000 --- a/src/framework/tasks/history.md +++ /dev/null @@ -1,34 +0,0 @@ - -Show workspace evolution over time. Read ROADMAP.md and present the chronological record of major workspace changes. - - - -As an AI builder, I want to see how my workspace has evolved, so that I can understand the trajectory and make informed decisions about future changes. - - - -- When user wants to review workspace history -- Entry point routes here via /base:history - - - - - -Read and present workspace evolution. - -1. Read `.base/ROADMAP.md` -2. Present chronologically: dates, what changed, why -3. Include audit summaries and major groom outcomes -4. If ROADMAP.md is empty or missing: "No history yet. Run /base:audit or /base:groom to start building your workspace timeline." - - - - - -Chronological workspace evolution timeline from ROADMAP.md. - - - -- [ ] History presented in clear chronological format -- [ ] Includes both audits and significant groom outcomes - diff --git a/src/framework/tasks/pulse.md b/src/framework/tasks/pulse.md deleted file mode 100644 index 8bf09b6..0000000 --- a/src/framework/tasks/pulse.md +++ /dev/null @@ -1,83 +0,0 @@ - -Daily workspace activation. Read workspace state, calculate drift, present health dashboard, prime the operator for their session. - - - -As an AI builder, I want a quick workspace health briefing at session start, so that I know what needs attention before I start working. - - - -- Start of every work session -- When user says "base pulse", "what's the state of things", "workspace status" -- When the pulse hook detects overdue grooming and injects a prompt -- Entry point routes here via /base:pulse - - - - - -Read workspace state from `.base/workspace.json` and `.base/data/state.json`. - -1. Read `.base/workspace.json` — the manifest -2. Read `.base/data/state.json` — the last known state -3. If either file is missing, suggest running `/base:scaffold` first -4. Extract: last groom date, groom cadence, area list, satellite list - - - -Check each tracked area against filesystem reality. - -For each area in the manifest: -1. Check filesystem timestamps on tracked paths (stat modification dates) -2. Compare against last groom date and area-specific cadence -3. Calculate days overdue (0 if within cadence) -4. Classify: Current (within cadence), Stale (1-2x overdue), Critical (2x+ overdue) - -For each registered satellite: -1. Check if state file exists and is readable -2. Extract last modification date -3. Report current phase if parseable - -Calculate total drift score: sum of days-overdue across all areas, with Critical areas weighted 2x. - - - -Present the health dashboard to the operator. - -Format: -``` -BASE Pulse — {workspace-name} -Last Groom: {date} ({N} days ago) -Drift Score: {score} - -| Area | Status | Age | Due | -|------|--------|-----|-----| -... - -Satellites: -| Project | Phase | Last Active | -... - -{Recommendation based on drift score} -``` - -Recommendations: -- Drift 0: "Workspace is clean. Proceed normally." -- Drift 1-7: "Minor drift in {areas}. Consider grooming this week." -- Drift 8-14: "Moderate drift. Run /base:groom soon." -- Drift 15+: "Critical drift. Workspace context is stale. Run /base:groom now." - - - - - -Health dashboard with drift score, area statuses, satellite health, and recommended next action. - - - -- [ ] All manifest areas checked against filesystem reality -- [ ] Drift score calculated correctly -- [ ] Satellites checked for health -- [ ] Clear recommendation provided based on drift level -- [ ] Dashboard is concise and scannable (not a wall of text) - diff --git a/src/framework/tasks/scaffold.md b/src/framework/tasks/scaffold.md deleted file mode 100644 index eab2aac..0000000 --- a/src/framework/tasks/scaffold.md +++ /dev/null @@ -1,389 +0,0 @@ - -Set up BASE in a new or existing workspace. Scan the workspace, ask guided questions, generate the manifest, install hooks, initialize JSON data surfaces, and run operator profile setup. Optional --full mode adds CLAUDE.md audit and guided first groom. - - - -As an AI builder setting up my workspace, I want a guided scaffolding process that configures workspace management for my specific setup, so that I get maintenance automation without manual configuration. - - - -- First-time BASE installation in any workspace -- When user says "base scaffold", "set up base", "initialize workspace management" -- Entry point routes here via /base:scaffold -- Use --full flag for batteries-included mode with CLAUDE.md audit + first groom - - - -@templates/workspace-json.md - - - - - -Determine scaffold mode. - -1. Check if user specified `--full` or mentioned wanting full setup -2. If `--full`: CLAUDE.md audit + first groom will be offered after data layer setup -3. If standard: data layer + hooks + operator profile -4. Announce mode: "Running BASE scaffold ({standard|full} mode)." - - - -Scan the workspace and detect what exists. - -1. List top-level directories and files -2. Detect common patterns: - - .base/data/ → existing JSON surfaces (v2 data model) - - ACTIVE.md, BACKLOG.md → legacy working memory (offer migration) - - projects/ → project tracking - - apps/ → satellite projects - - tools/ → tool management - - .claude/ → system layer - - .mcp.json → MCP configuration - - content/ → content pipeline - - clients/ → client work - - obsidian/ → knowledge graph - - .carl/ → CARL dynamic rules -3. Detect satellite projects (directories with .paul/ inside apps/) -4. Present findings: "I found: {list of detected areas}" - -**Wait for confirmation before proceeding.** - - - -Walk through each detected area and configure tracking. - -For each detected area: -1. "I found {area}. Want BASE to track this?" -2. If yes: "What grooming cadence? (weekly/bi-weekly/monthly)" -3. Auto-select audit strategy based on area type -4. Allow override of defaults - -Also ask: -- "What day do you prefer for weekly grooming?" (default: Friday) -- "Any directories I should scan for satellite projects?" (default: apps/) -- "Anything else you want tracked that I didn't detect?" - -Build workspace.json from responses using `@templates/workspace-json.md` schema. - - - -Create .base/ directory and generate JSON data surfaces. - -1. Create `.base/` directory structure: - ``` - .base/ - ├── workspace.json - ├── operator.json - ├── data/ - │ ├── projects.json - │ ├── entities.json - │ ├── state.json - │ ├── psmm.json - │ └── staging.json - ├── hooks/ - ├── base-mcp/ - ├── grooming/ - ├── schemas/ - └── audits/ - ``` -2. Write workspace.json from guided configuration (with surfaces and carl_hygiene sections) -3. Initialize JSON data surfaces with empty starter content (don't overwrite existing): - - projects.json — unified active work + backlog tracking - - entities.json — people, organizations, systems - - state.json — workspace health, drift, groom tracking - - psmm.json — per-session meta memory - - staging.json — proposed changes staging -4. Copy operator.json template (don't overwrite existing) -5. Register any detected satellite projects in workspace.json -6. Report: "BASE data layer installed. {N} areas tracked, {N} satellites registered, {N} data surfaces initialized." - - - -Install and register BASE hooks. - -All hooks live in `.base/hooks/`. Session hooks are registered in `.claude/settings.json`. - -**UserPromptSubmit hooks** (fire every prompt): -- active-hook.py — active work surface injection -- backlog-hook.py — backlog surface injection -- base-pulse-check.py — drift detection + groom reminders -- psmm-injector.py — per-session meta memory injection -- operator.py — operator identity context injection - -**SessionStart hooks** (fire once when Claude Code starts a session): -- satellite-detection.py — PAUL project auto-registration and state sync - -**On-demand hooks** (invoked by commands, not auto-registered): -- apex-insights.py — workspace analytics (invoked by /apex:insights) - ---- - -### ENVIRONMENT DETECTION (REQUIRED — do this FIRST) - -Hooks are shell commands that Claude Code executes. The python path AND file paths must work in the context where Claude Code is running. Detect the environment before wiring anything. - -**Step 1: Identify the platform.** -Run these commands and read the results: -```bash -uname -a # Linux vs Darwin vs MINGW/MSYS -cat /proc/version 2>/dev/null # WSL detection (contains "Microsoft" or "WSL") -echo $TERM_PROGRAM # vscode = VS Code integrated terminal -``` - -**Step 2: Classify the environment.** - -| Environment | Detection | Python Command | File Paths | -|---|---|---|---| -| **Native Linux** | `uname` = Linux, no WSL in /proc/version | `which python3` → use result | Native paths work | -| **Native macOS** | `uname` = Darwin | `which python3` → use result (often /opt/homebrew/bin/python3) | Native paths work | -| **WSL Terminal** (Claude Code CLI in WSL) | Linux + "Microsoft" in /proc/version + NOT in VS Code | `which python3` → use result (typically /usr/bin/python3) | WSL paths work (/home/user/...) | -| **VS Code Extension (WSL Remote)** | Linux + WSL + TERM_PROGRAM=vscode | `which python3` → use result | WSL paths work (VS Code server runs inside WSL) | -| **VS Code Extension (Windows-native)** | platform: win32 in Claude Code, OR `uname` returns MINGW/MSYS | See troubleshooting below | Windows paths required | -| **Native Windows** | No WSL, Windows paths | `where python` or `py -3` | Windows paths (C:\...) | - -**Step 3: Handle the tricky cases.** - -**VS Code Extension on Windows accessing WSL files (PROBLEMATIC):** -This is the hardest case. The VS Code extension runs on the Windows side but can see WSL files. Hooks execute in a Windows context, so: -- `/usr/bin/python3` does NOT exist -- `/home/user/...` paths are NOT valid -- The Windows Python stub (`WindowsApps/python3.exe`) can't access WSL paths - -**Solutions (present to user in order of preference):** - -1. **Use VS Code Remote - WSL extension** (RECOMMENDED): - - Install the "WSL" extension in VS Code (by Microsoft) - - Open the workspace with "Reopen in WSL" or `code --remote wsl+Ubuntu /path/to/workspace` - - This runs the VS Code server inside WSL — all hooks fire natively - - All WSL paths and python work correctly - -2. **Use Claude Code CLI in WSL terminal instead of VS Code extension:** - - Open a WSL terminal, `cd` to workspace, run `claude` - - All hooks fire natively in WSL context - - Use VS Code separately for editing if needed - -3. **Wrapper script approach** (for advanced users who need both contexts): - Create a wrapper at a Windows-accessible location that detects context and routes: - ```bash - #!/bin/bash - # Detect if running in WSL or Windows and route accordingly - if [ -f /proc/version ] && grep -qi microsoft /proc/version; then - # Running in WSL context — use WSL python directly - /usr/bin/python3 "$@" - else - # Running in Windows context — invoke via wsl - wsl /usr/bin/python3 "$@" - fi - ``` - This is fragile and NOT recommended for most users. - -**IMPORTANT: Ask the user which environment they use Claude Code in before proceeding.** -If they use multiple environments (e.g., CLI in WSL + VS Code extension), explain the constraints and recommend option 1 (VS Code Remote WSL). - ---- - -### HOOK REGISTRATION - -After environment is classified and python path is determined: - -For each auto-fire hook: -1. Check if `.base/hooks/{hook}` exists -2. If not: copy from `~/.claude/base-framework/hooks/{hook}` (global install source) - - If `~/.claude/base-framework/hooks/{hook}` doesn't exist either, warn: - "BASE framework not globally installed. Run `npx base-framework --global` first, then re-run scaffold." -3. Check `.claude/settings.json` for hook registration: - - **UserPromptSubmit hooks** → register in `UserPromptSubmit` array - - **SessionStart hooks** (satellite-detection.py) → register in `SessionStart` array -4. If not registered: add the hook entry using detected python path + absolute path to `.base/hooks/{hook}` - -Hook registration format in settings.json: - -**CRITICAL: Each event type array contains objects with a `hooks` array inside — NOT flat command objects.** This is the Claude Code settings.json schema. Getting this wrong means hooks silently fail. - -```json -{ - "hooks": { - "UserPromptSubmit": [ - { - "hooks": [ - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/active-hook.py" }, - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/backlog-hook.py" }, - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/base-pulse-check.py" }, - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/psmm-injector.py" }, - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/operator.py" } - ] - } - ], - "SessionStart": [ - { - "hooks": [ - { "type": "command", "command": "{detected_python3_path} {absolute_path_to_workspace}/.base/hooks/satellite-detection.py" } - ] - } - ] - } -} -``` - -**Merge strategy:** If `.claude/settings.json` already has a `hooks` section with existing entries (e.g., from CARL or other tools), APPEND BASE hooks into the existing `hooks` arrays inside the event type objects. Do NOT overwrite existing hooks. Read the file first, find the right array, add entries that aren't already present. - ---- - -### HOOK TROUBLESHOOTING - -If hooks aren't firing after setup, diagnose with these checks: - -**Symptom: "operation blocked by hook" or "No such file"** -- Python path is wrong for the current environment -- Fix: re-detect python path for the environment Claude Code is running in - -**Symptom: Zero hooks fire (no CARL, no pulse, no calendar, nothing)** -- Likely a platform mismatch (Windows paths vs WSL paths) -- Check: `echo $PATH | tr ':' '\n' | grep python` — does python3 resolve? -- Check: Can Claude Code's shell access the hook file? Run `cat {hook_path}` to verify - -**Symptom: Hooks fire in terminal but not in VS Code (or vice versa)** -- Different Claude Code instances run in different contexts -- VS Code extension (Windows-native) ≠ Claude Code CLI (WSL) -- Fix: Use VS Code Remote WSL extension so both contexts are WSL - -**Symptom: "python3: command not found"** -- Python3 is not on PATH in the hook execution context -- Fix: Use absolute path to python3 (detect with `which python3`) - -**Diagnostic command (run this to check hook health):** -```bash -# Test each hook manually -for hook in .base/hooks/*.py; do - echo "--- Testing: $hook ---" - {detected_python3_path} "$hook" 2>&1 | head -3 - echo "Exit code: $?" -done -``` - -Report: "Hooks installed ({N} auto-fire hooks registered, 1 on-demand hook available)." -Report environment: "{environment_type} detected — hooks configured for {python_path}" - - - -Guide the operator through their profile setup. - -1. Check if `.base/operator.json` has completed sections (check `completed_at` fields) -2. If all sections completed: "Operator profile already configured. Want to update any section?" -3. If incomplete or new: - - Walk through each section of operator.json: - a. **Deep Why** — 5 progressively deeper questions about motivation - b. **North Star** — One measurable metric with timeframe - c. **Key Values** — Rank-ordered values with concrete meanings (max 5) - d. **Elevator Pitch** — Layered pitch (1-4 floors) - e. **Surface Vision** — Concrete scenes of what success looks like - - Each section can be skipped: "Skip for now? You can complete it later." - - Write responses to operator.json after each section -4. Report: "Operator profile {complete|partially complete}. The operator hook will inject your identity context every session." - - - -Install and wire the MCP server from the global BASE package. - -The MCP server package lives globally at `~/.claude/base-framework/packages/base-mcp/`. Scaffold copies it into the workspace and wires it up. - -1. Check if `.base/base-mcp/index.js` exists in the workspace -2. If NOT present: - a. Check if global source exists at `~/.claude/base-framework/packages/base-mcp/` - b. If global source missing: warn "BASE framework not globally installed. Run the installer first." - c. If global source exists: copy the entire `base-mcp/` directory to `.base/base-mcp/` - d. Run `npm install` in `.base/base-mcp/` to install dependencies -3. If already present: check for `node_modules/`. If missing, run `npm install`. -4. Check `.mcp.json` for base-mcp registration -5. If not registered: add registration to `.mcp.json`: - ```json - { "base-mcp": { "type": "stdio", "command": "node", "args": ["./.base/base-mcp/index.js"] } } - ``` -6. Report: "BASE MCP server installed from global package and registered. Claude can now manage your data surfaces through tool calls." - - - -**Full mode only.** - -**CLAUDE.md audit:** -1. Check if CLAUDE.md exists -2. If exists: "Want me to audit your CLAUDE.md against the CLAUDE.md Strategy?" - - If yes: route to `/base:audit-claude-md` (interactive, strategy-driven audit with CARL detection) -3. If doesn't exist: "Want me to generate a CLAUDE.md from the strategy template?" - - If yes: use `@~/.claude/base-framework/templates/claudemd-template.md` as starting point, fill from detected workspace structure - -**First groom:** -1. "Want to run an initial groom to establish baseline? This reviews each area once." -2. If yes: run /base:groom flow -3. If no: "Baseline set from filesystem timestamps. First groom due: {date}." - - - -Quick review and cleanup. Catches artifacts from path detection bugs, stale files, or misaligned structure. - -**Run these checks in order:** - -1. **Bogus directories** — Scan workspace root for directories that shouldn't exist: - - Any directory starting with `C:` or containing Windows-style paths (path detection bug) - - Any directory named `undefined`, `null`, or `[object Object]` - - If found: delete them and report what was removed - -2. **Sunset files** — Check for files that no longer belong: - - `.base/data/active.json` → sunset, replaced by projects.json - - `.base/data/backlog.json` → sunset, replaced by projects.json - - `ACTIVE.md` or `BACKLOG.md` at workspace root → legacy, offer to remove - - If found: report and offer to remove (don't auto-delete without confirmation) - -3. **Path sanity** — Verify all registered hooks use correct paths for the current environment: - - Read `.claude/settings.json` hook entries - - Check each hook path exists on disk - - Check python path resolves (`which {python_path}`) - - If any path is invalid: flag it with the correct replacement - -4. **MCP sanity** — Verify MCP registration points to a real file: - - Read `.mcp.json` - - Check `.base/base-mcp/index.js` exists - - Check `node_modules/` exists in `.base/base-mcp/` - - If broken: fix it (copy from global, npm install, re-register) - -5. **Structure alignment** — Verify workspace matches CLAUDE.md's Where section: - - Read CLAUDE.md (if it exists) and extract the Where section - - Compare declared directories against what actually exists - - Flag any mismatches (declared but not created, or created but not declared) - - Don't auto-fix — just report for user awareness - -**Report:** -``` -Post-scaffold cleanup: -- Artifacts removed: {list or "none"} -- Sunset files found: {list or "none"} -- Hook paths: {all valid | N issues} -- MCP: {healthy | issues} -- Structure alignment: {aligned | N mismatches} -``` - -If everything is clean: "Workspace is clean. No artifacts, all paths valid, structure aligned." - - - - - -Fully configured BASE installation. Standard mode: data layer with JSON surfaces, hooks wired, operator profile setup, MCP registered, post-scaffold cleanup verified. Full mode: adds CLAUDE.md audit and guided first groom. - - - -- [ ] Workspace scanned and areas detected -- [ ] Operator confirmed tracked areas and cadences -- [ ] .base/ directory created with all required files -- [ ] workspace.json generated from guided configuration -- [ ] JSON data surfaces initialized (projects, entities, state, psmm, staging) -- [ ] operator.json created and profile questionnaire offered -- [ ] Satellite projects detected and registered -- [ ] All auto-fire hooks installed and registered in settings.json (UserPromptSubmit + SessionStart) -- [ ] BASE MCP server wired in .mcp.json -- [ ] Post-scaffold cleanup passed (no artifacts, valid paths, structure aligned) -- [ ] (Full mode) CLAUDE.md audit offered -- [ ] (Full mode) First groom offered -- [ ] Operator informed of next groom date - diff --git a/src/framework/tasks/status.md b/src/framework/tasks/status.md deleted file mode 100644 index 55498de..0000000 --- a/src/framework/tasks/status.md +++ /dev/null @@ -1,35 +0,0 @@ - -Quick one-liner workspace health check. No conversation, just the numbers. - - - -As an AI builder, I want a fast health check I can glance at, so that I know if anything needs attention without a full briefing. - - - -- When user wants a quick check without full pulse -- Entry point routes here via /base:status - - - - - -Read state and output one-liner. - -1. Read `.base/data/state.json` -2. Calculate current drift score from timestamps -3. Count overdue areas and past-due backlog items -4. Output single line: "BASE: Drift {score} | {N} areas overdue | {N} backlog items past review-by | Last groom: {date}" - - - - - -Single-line health summary. No conversation. - - - -- [ ] Output is one line -- [ ] Drift score is current (not cached) -- [ ] Overdue counts are accurate - diff --git a/src/framework/tasks/surface-convert.md b/src/framework/tasks/surface-convert.md deleted file mode 100644 index 372aebd..0000000 --- a/src/framework/tasks/surface-convert.md +++ /dev/null @@ -1,143 +0,0 @@ - -Convert an existing @-mentioned markdown file into a structured data surface. Analyzes the markdown structure, proposes a schema, migrates content, and generates all surface artifacts (JSON, hook, registration). - - - -As an AI builder, I want to convert my existing markdown tracking files into structured surfaces so Claude gets cheap passive awareness instead of expensive @-file parsing. - - - -- /base:surface convert {file-path} -- "convert this file to a surface", "make this a data surface" -- User has a markdown file they @-mention regularly and wants it structured - - - -@.base/hooks/_template.py -@.base/workspace.json - - - - - -## Step 1: Read & Analyze - -1. Read the specified markdown file completely -2. Detect structure: - - **Headings** → potential categories or priority groups - - **Bold labels** (e.g., `**Status:**`) → field names - - **List items** → individual entries - - **Checkboxes** → checklist/progress fields - - **Dates** → timestamp fields - - **File paths** → location fields - - **Tables** → structured data (archived items, reference tables) -3. Identify patterns: - - How many distinct items? - - What fields recur across items? - - Are there priority/status groupings? - - Is there an archived/done section? - -Present findings: -``` -Detected structure in {file}: - Items found: {count} - Sections: {list of heading-based groups} - Recurring fields: {field names} - Archived section: {yes/no} -``` - - - -## Step 2: Propose Schema - -Based on analysis, propose: - -1. **Surface name** — infer from filename (e.g., ACTIVE.md → "active") -2. **Schema** — field names, types, required fields, ID prefix -3. **Sample conversion** — show 2-3 items converted to JSON - -``` -Proposed schema for "{name}": - ID prefix: {PREFIX} - Required: {fields} - Optional: {fields} - Priority levels: {if detected} - -Sample conversion: - "{heading item}" → - { - "id": "PREFIX-001", - "title": "...", - "status": "...", - ... - } -``` - -Ask: "Does this schema look right? Adjust anything?" - -**Wait for response.** - - - -## Step 3: Confirm - -Apply any user adjustments to the schema. Lock it for generation. - -If user is satisfied, confirm: -"Schema locked. I'll generate the surface and migrate {count} items." - - - -## Step 4: Generate Artifacts - -Same generation as surface-create Step 5: -- `.base/data/{name}.json` — with migrated items (not empty) -- `.base/hooks/{name}-hook.py` — from _template.py with appropriate grouping -- workspace.json surface registration -- settings.json hook entry - -Include staleness detection with sensible defaults based on detected priority levels. - - - -## Step 5: Migrate Content - -Parse every item from the markdown into JSON entries: -- Map heading groups to priority/category fields -- Map bold labels to field values -- Preserve checklists as arrays of {text, done} objects -- Preserve dates in ISO format -- Preserve file paths as location fields -- Map done/closed/archived sections to the archived array - -Report: -``` -Migration complete: - Items migrated: {count} - Archived items: {count} - Unmapped items: {count, if any — list them} -``` - -If any items couldn't be auto-mapped, present them for manual resolution. - - - -## Step 6: Clean Up - -1. Check if the original file is @-referenced in CLAUDE.md -2. If found, offer: "Remove @{file} from CLAUDE.md? The surface hook replaces it." -3. Suggest: "Original file preserved at {path} for reference." - -Do NOT delete the original markdown file — the user decides its fate. - - - - - -After conversion: -- [ ] .base/data/{name}.json exists with migrated items -- [ ] Item count matches source markdown -- [ ] .base/hooks/{name}-hook.py exists and produces output -- [ ] workspace.json and settings.json updated -- [ ] Original markdown file is untouched - diff --git a/src/framework/tasks/surface-create.md b/src/framework/tasks/surface-create.md deleted file mode 100644 index c679bb3..0000000 --- a/src/framework/tasks/surface-create.md +++ /dev/null @@ -1,184 +0,0 @@ - -Create a new data surface through guided conversation. Generates: JSON data file, injection hook, workspace.json registration, and settings.json hook entry. The user answers questions; Claude generates everything. - - - -As an AI builder, I want to create custom data surfaces so Claude has structured, passive awareness of any domain I track — without manually wiring JSON files, hooks, and config. - - - -- /base:surface create {name} -- "create a surface", "add a new surface", "I want to track X" -- User wants structured data with hook injection and MCP access - - - -@.base/hooks/_template.py -@.base/workspace.json - - - - - -## Step 1: Define - -Extract surface name from args or ask: "What should this surface be called? (lowercase, no spaces)" - -**Validate:** -1. Name is lowercase, alphanumeric + hyphens only -2. Not already registered in workspace.json surfaces section -3. No reserved names: "active", "backlog", "psmm", "staging" - -Ask: "What does this surface track? (one sentence)" -→ This becomes the `description` in workspace.json. - -**Wait for response before proceeding.** - - - -## Step 2: Schema - -Ask: "What fields does each item need?" - -Guide through these decisions (one at a time): - -1. **Required fields** — What must every item have? - - Default minimum: `["title"]` - - Common additions: status, priority, category, assignee, due_date - -2. **ID prefix** — Auto-suggest first 3 chars of name, uppercase. - - e.g., surface "clients" → prefix "CLI" - - User can override - -3. **Priority/status enums** — If the surface has priority or status fields: - - Ask: "What priority levels?" (e.g., high, medium, low) - - Ask: "What status values?" (e.g., active, pending, done) - -4. **Time rules** (optional) — If the surface benefits from review-by dates: - - Ask: "Should items have review-by deadlines? If so, how many days per priority level?" - - Default: none - -Build the schema object from answers: -```json -{ - "id_prefix": "CLI", - "required_fields": ["title", "status"], - "priority_levels": ["high", "medium", "low"], - "status_values": ["active", "pending", "done"] -} -``` - -**Wait for response before proceeding.** - - - -## Step 3: Injection - -Ask: "How should items appear in Claude's context each prompt?" - -Guide through: - -1. **Grouping** — How to organize items in the injection? - - By priority (default) | By status | By date | Flat (no grouping) - -2. **Summary format** — What fields to show per line? - - Default: `- [ID] Title (status)` - - Can add: priority tag, due date, custom field - -3. **Staleness thresholds** — Days before an item is flagged STALE per priority: - - Suggest defaults based on priority levels from Step 2 - - e.g., high: 5d, medium: 10d, low: 30d - - User can adjust - -4. **Behavioral mode:** - - Silent (default) — passive awareness, respond only when asked - - Proactive — mention items unprompted (rare, use for critical surfaces) - - Threshold — stay silent unless deadline/staleness threshold crossed - -**Wait for response before proceeding.** - - - -## Step 4: Tools (Informational) - -Inform the user: - -"All 7 BASE MCP tools work automatically with your new surface: -- `base_list_surfaces` — see all surfaces -- `base_get_surface("{name}")` — read all items -- `base_get_item("{name}", id)` — get specific item -- `base_add_item("{name}", data)` — add new item (validates required fields, auto-generates ID) -- `base_update_item("{name}", id, data)` — update fields (resets staleness clock) -- `base_archive_item("{name}", id)` — move to archived -- `base_search(query, "{name}")` — search items - -No configuration needed — BASE MCP auto-discovers surfaces from workspace.json." - -**Continue to generation.** - - - -## Step 5: Generate - -Create all artifacts: - -**1. Data file:** `.base/data/{name}.json` -```json -{ - "surface": "{name}", - "version": 1, - "last_modified": "{timestamp}", - "items": [], - "archived": [] -} -``` - -**2. Hook file:** `.base/hooks/{name}-hook.py` -- Read `.base/hooks/_template.py` as the starting point -- Customize SURFACE_NAME, grouping logic, summary format, staleness thresholds, behavioral directive -- Use the injection decisions from Step 3 -- Include `from datetime import date` for staleness calculation -- Follow the _template.py contract exactly - -**3. Registration:** Add to `.base/workspace.json` surfaces section: -```json -"{name}": { - "file": "data/{name}.json", - "description": "{description from Step 1}", - "hook": true, - "silent": true, - "schema": { ...schema from Step 2... } -} -``` - -**4. Hook registration:** Add to `.claude/settings.json` UserPromptSubmit hooks: -```json -{ - "type": "command", - "command": "python3 {absolute_workspace_path}/.base/hooks/{name}-hook.py" -} -``` -Use absolute path resolved from workspace root. - -**5. Report:** -``` -Surface "{name}" created: - Data: .base/data/{name}.json (empty, ready for items) - Hook: .base/hooks/{name}-hook.py (will inject next prompt) - Schema: {id_prefix}-NNN, {required_fields} - Tools: base_get_item("{name}", id), base_add_item("{name}", data), etc. - -Add your first item: base_add_item("{name}", {title: "..."}) -``` - - - - - -After generation: -- [ ] .base/data/{name}.json exists and is valid JSON -- [ ] .base/hooks/{name}-hook.py exists and parses as valid Python -- [ ] workspace.json has the surface registration -- [ ] settings.json has the hook entry with absolute path -- [ ] Hook outputs correct XML when piped test input - diff --git a/src/framework/tasks/surface-list.md b/src/framework/tasks/surface-list.md deleted file mode 100644 index ce154f6..0000000 --- a/src/framework/tasks/surface-list.md +++ /dev/null @@ -1,42 +0,0 @@ - -Display all registered data surfaces with item counts, hook status, and staleness summary. - - - -- /base:surface list -- "what surfaces exist", "show surfaces", "list surfaces" - - - - - -## Show Surfaces - -1. Call `base_list_surfaces` to get all registered surfaces with item counts -2. For each surface, read the data file to count stale items (items with no `updated` field or `updated` older than threshold) - -Display as: - -``` -Data Surfaces -═══════════════════════════════════════ - -| Surface | Items | Hook | Description | -|---------|-------|------|-------------| -| active | 12 | ✓ | Active work items, projects, and tasks | -| backlog | 8 | ✓ | Future work queue, ideas, and deferred tasks | - -Total: {count} surfaces, {total_items} items - -Create a new surface: /base:surface create {name} -Convert a file: /base:surface convert {path} -``` - -If no surfaces registered: -``` -No data surfaces registered. -Run /base:surface create {name} to create your first surface. -``` - - - diff --git a/src/framework/tasks/weekly-domain-create.md b/src/framework/tasks/weekly-domain-create.md deleted file mode 100644 index 80772b9..0000000 --- a/src/framework/tasks/weekly-domain-create.md +++ /dev/null @@ -1,173 +0,0 @@ - -Guided creation of a custom domain phase for /base:weekly. Walks the user through defining what to check, what tools to pull data from, what questions to ask weekly, where the phase fits in the flow, and what output it produces. Every domain phase goes through this same workflow — no presets, no shortcuts. - - - -As an AI builder, I want to add custom check-in areas to my weekly ritual, so that the weekly covers everything that matters to me without being locked into someone else's priorities. - - - -- When operator wants to add a domain to their weekly -- After first /base:weekly run (offered when no domain phases exist) -- When operator says "add a domain to my weekly", "I want to track X weekly" -- Entry point routes here via /base:weekly-domain - - - - - -Understand what the operator wants to track weekly. - -1. Ask: "What area of your week needs its own check-in?" -2. Offer starter ideas to help them think clearly: - - "Some common ones people create: revenue tracking, content pipeline, client follow-ups, team sync, health/fitness check, learning log, community engagement." - - "Or describe something entirely your own." -3. Wait for response -4. Clarify if needed: "So this phase would check on {paraphrase}. What would make this useful to you every week?" - -**Wait for clear description before proceeding.** - - - -Identify data sources and tools for this domain. - -1. Ask: "What data should this phase pull? What tools or sources are relevant?" - - Give examples: "Calendar events in a category, Slack channel messages, MCP server data, project statuses, external dashboard" -2. If the operator is unsure: - a. Search available MCP tools (use tool listing / grep for relevant keywords) - b. Present relevant ones: "I found these tools that might be useful: {list}" - c. Let operator pick or decline -3. If a needed tool doesn't exist: - - Note it: "That tool doesn't exist yet. I'll note it as a future backlog item." - - The domain phase still works — it just skips that data source at runtime -4. For each selected data source, capture: - - Tool name (MCP tool ID) - - Parameters to pass - - Human-readable label - -**Build the data_sources[] array from this conversation.** - - - -Define the weekly questions this phase asks. - -1. Ask: "What questions should this phase ask you every week?" - - Give examples based on their domain: - - Revenue: "Did I hit my revenue target? What's the pipeline value? Any invoices pending?" - - Content: "How many pieces published? What's queued? Am I on cadence?" - - Custom: derive from their description -2. Capture each question as a string -3. Confirm the list: "These are the questions for your {name} phase: {list}. Adjust?" - -**Wait for confirmation.** - - - -Determine where this phase runs in the weekly flow. - -Present the weekly structure: -``` -1. Week Review -2. Calendar Audit -3. Workspace Groom -4. Priority Stack -5. Backlog Triage - --- domain phases run here --- -6. Blockers & Delegation -7. Week Commit -``` - -Ask: "Where should this phase run? Most domain phases run after backlog triage (position 5) and before blockers (position 6). Sound right, or do you want it somewhere else?" - -Options: -- `after_groom` — runs after Phase 3 -- `after_priorities` — runs after Phase 4 -- `after_backlog` — runs after Phase 5 (default, recommended) -- `before_blockers` — same as after_backlog unless other domains exist (controls ordering among domains) - -**Wait for response.** - - - -Determine what this phase produces. - -Ask: "What should this phase output?" -Options: -- **Notes** — just captures your responses as part of the weekly record -- **Calendar events** — creates specific calendar blocks (e.g., "content recording session") -- **Project updates** — updates project statuses or adds tasks -- **Mixed** — any combination - -For each output type, capture specifics: -- Calendar: what kind of events, default duration, which calendar -- Project: which project ID to update, what fields -- Notes: just stored in weekly entry (default, always happens) - -**Wait for response.** - - - -Generate the domain phase config and save to weekly.json. - -1. Compose the domain phase object: - ```json - { - "id": "{kebab-case-id}", - "name": "{Display Name}", - "description": "{one-line description}", - "position": "{chosen position}", - "data_sources": [ - { - "type": "mcp", - "tool": "{tool_name}", - "params": {}, - "label": "{human label}" - } - ], - "questions": [ - "{question 1}", - "{question 2}" - ], - "outputs": { - "type": "notes|calendar|project_update|mixed", - "details": {} - }, - "enabled": true, - "created": "{ISO date}" - } - ``` -2. Read current `.base/weekly.json` -3. Append to `domain_phases[]` -4. Write updated weekly.json -5. Present: - ``` - Domain phase created: {name} - Position: {where it runs} - Data sources: {list} - Questions: {count} - Output: {type} - - This will run during your next /base:weekly. - Want to create another domain phase, or are you done? - ``` - -**Wait for response.** - - - - - -New domain phase config added to `.base/weekly.json` -> `domain_phases[]`. Ready to execute on next /base:weekly run. - - - -- [ ] Operator described the domain area clearly -- [ ] Tool discovery performed — available MCP tools searched if operator was unsure -- [ ] Data sources identified and captured with tool names + params -- [ ] Weekly questions defined and confirmed -- [ ] Position in weekly flow chosen -- [ ] Output type specified -- [ ] Domain phase config written to weekly.json -- [ ] Config is valid JSON matching the schema -- [ ] Operator informed of next steps - diff --git a/src/framework/tasks/weekly.md b/src/framework/tasks/weekly.md deleted file mode 100644 index b293e95..0000000 --- a/src/framework/tasks/weekly.md +++ /dev/null @@ -1,347 +0,0 @@ - -Guided weekly review and planning ritual. Walks the operator through 8 core phases sequentially, executes custom domain phases, and commits the week with calendar events created. Designed as one of two cadence rituals (daily closes the day, weekly closes the week and opens the next). - - - -As an AI builder, I want a structured weekly ritual that reviews my week, plans the next, maintains my workspace, and locks in priorities with real calendar events, so that I start every week with clarity instead of reacting to whatever's loudest. - - - -- Weekly (typically Sunday evening) -- When operator says "run my weekly", "weekly review", "time for my weekly" -- Entry point routes here via /base:weekly - - - - - -Load config and establish session context. - -1. Read `.base/weekly.json` - - If file doesn't exist: create it with empty defaults (see schema below) - - Parse: domain_phases[], calendar_rules[], daily_logs[], history[] -2. Get last weekly entry from history[] (if any) - - Note: week_of, priorities set, priorities completed -3. Get current date context (from hook injection or system) -4. Present: - ``` - Weekly — Week of {Monday date} to {Sunday date} - Last weekly: {date or "first run"} - Daily logs this week: {count} - Domain phases configured: {count} - ``` -5. "Ready to start? (You can skip any phase by saying 'skip')" - -**Wait for confirmation.** - - - -Phase 1: Review the past week. - -**If daily logs exist** (daily_logs[] entries from the past 7 days): -1. Summarize: days logged, patterns in wins/misses, energy trends -2. Note any recurring blockers or themes -3. Present the summary to the operator - -**If no daily logs:** -1. Note: "No daily logs found this week. Phase 1 is reflective-only." - -**Always ask:** -- "What went well this week?" -- "What didn't land?" -- "Anything surprising or worth noting?" - -Capture the operator's reflection. This goes into the weekly entry. - -**Wait for responses.** - - - -Phase 2: Audit and plan the calendar. - -1. Use `list_events` MCP tool — pull events for the next 7 days - - If multiple calendars available (personal + family), pull all - - If calendar MCP unavailable: skip to manual questions, note the gap -2. Apply display rules: show all events with real titles during audit (rules only apply on creation) -3. Present the week's schedule in a clean format: - ``` - Monday 3/31: - 9:00 AM — Meeting with Charlie - 2:00 PM — Coaching call (Amee) - Tuesday 4/1: - (open) - ... - ``` -4. Ask: - - "Anything missing from the calendar?" - - "Where do you want deep work blocks?" - - "Any family commitments to add?" - - "Any conflicts to resolve?" -5. Collect requested additions/changes (created in Phase 8) - -**Wait for responses.** - - - -Phase 3: Workspace maintenance. - -Run groom logic inline — NOT as a separate /base:groom invocation. - -1. Read drift score from `base_get_state` or state.json -2. Read stale areas from base-pulse data -3. Walk through each stale area: - - Projects: quick status check on active/blocked items - - Clients: any updates needed? - - Content: pipeline current? - - Other flagged areas from pulse -4. For each area reviewed: - - Update timestamps via `base_update_area` - - Note changes made -5. Record groom via `base_record_groom` -6. Update drift via `base_update_drift` -7. Report: "Drift score: {before} -> {after}. {N} areas groomed." - -**Voice-friendly: walk through one area at a time, wait for response on each.** - - - -Phase 4: Set the week's priorities. - -1. Pull context: - - Operator north star (from operator.json / hook data) - - Active projects with upcoming deadlines (from active-awareness) - - Stale urgent/high items - - Last week's priorities and their status (from previous weekly entry) -2. If previous priorities exist: - - Report: "Last week's priorities: {list}. Status: {completed/carried/dropped}" -3. Suggest 3-5 outcome-based priorities: - - Frame as outcomes, not tasks: "Ship X" not "Work on X" - - Align each to north star or active project - - Weight toward revenue-generating and deadline-driven work -4. Present: "Here are my suggested priorities for this week: {list}. Adjust?" -5. Finalize the stack after operator input - -**Wait for approval or adjustments.** - - - -Phase 5: Process the backlog. - -1. Pull backlog items via `base_list_projects` (status=backlog) -2. Identify items with: - - review_by date passed (overdue) - - review_by date within 7 days (upcoming) - - No review_by date and older than 14 days (stale) -3. Present overdue items first: "These are past their review date: {list}" -4. For each flagged item, ask: - - **Keep** — set new review_by date - - **Graduate** — move to active (update status via `base_update_project`) - - **Kill** — archive via `base_archive_project` -5. After processing flagged items: "Anything new to add to the backlog?" -6. Capture new items via `base_add_project` (status=backlog) - -**Walk through one item at a time.** - - - -Phase 6: Execute custom domain phases. - -1. Read `weekly.json` -> `domain_phases[]` (only enabled ones) -2. If no domain phases configured: - - "No domain phases configured. You can add one anytime with /base:weekly-domain." - - Skip to Phase 7 -3. Sort by configured position (after_groom, after_priorities, after_backlog, before_blockers) -4. For each domain phase: - a. Announce: "Domain phase: {name} — {description}" - b. Pull data from configured data_sources[]: - - For each source: call the specified MCP tool with configured params - - If tool unavailable: note it, continue with remaining sources - c. Present pulled data - d. Ask configured questions[] one at a time - e. Capture responses - f. Produce configured output (notes, calendar events, project updates) -5. Store domain phase results in weekly entry -> domains{} - -**Each domain phase is self-contained — failure in one doesn't block others.** - - - -Phase 7: Identify blockers and queue follow-ups. - -**If Slack MCP available:** -1. Pull recent messages from relevant channels/DMs (last 7 days) -2. Surface threads with pending action or unanswered questions -3. Present: "Here are open threads that may need follow-up: {list}" -4. For each: "Send a nudge this week? (becomes a calendar reminder or note)" - -**If Slack MCP unavailable:** -1. Ask: "What's currently blocked?" -2. Ask: "Who do you need to follow up with this week?" - -**Always:** -3. Cross-reference with active projects that have `blocked` status -4. Generate follow-up list: person, action, urgency -5. Ask which follow-ups should become calendar reminders - -**Wait for responses.** - - - -Phase 8: Lock in the week. - -1. Summarize everything from this session: - ``` - WEEK COMMIT — {date range} - - Priorities: - 1. {outcome} - 2. {outcome} - ... - - Calendar changes: - - {new event/block} on {day} - ... - - Groom: Drift {before} -> {after} - Backlog: {N} reviewed, {N} graduated, {N} killed, {N} new - Follow-ups: {list} - ``` -2. Ask: "Confirm? I'll create the calendar events and log the weekly." - -**On confirmation:** -3. Create calendar events via `create_event` MCP tool: - - Deep work blocks - - Follow-up reminders - - Any additions from Phase 2 - - Apply calendar_rules[] to event titles on creation: - - For each rule where type=title_transform and enabled=true: - - If event title matches rule.match regex: replace with rule.replace - - If rule.calendars specified: only apply to those calendars -4. Write weekly entry to `weekly.json` -> `history[]`: - - Include all phase outputs (review, calendar, groom, priorities, backlog, domains, blockers) - - Compute changeover metrics vs previous entry (priorities carried/completed/dropped, drift delta, backlog net) -5. Report: - ``` - Weekly complete. - {N} calendar events created. - Drift score: {X}. - Next weekly: {suggested date}. - ``` - -**Wait for confirmation before creating events.** - - - - - - -## weekly.json — Initial Empty Config - -When weekly.json doesn't exist, create with: - -```json -{ - "version": "1.0", - "created": "{ISO date}", - "calendar_rules": [], - "domain_phases": [], - "daily_logs": [], - "history": [] -} -``` - -## Daily Log Entry Schema (forward-looking — consumed by Phase 1, written by /base:daily) - -```json -{ - "date": "YYYY-MM-DD", - "logged_at": "ISO datetime", - "reflection": "string", - "wins": ["string"], - "misses": ["string"], - "energy": "high|medium|low", - "domains": { - "domain_id": { - "activities": ["string"], - "metrics": {} - } - }, - "blockers_surfaced": ["string"], - "tomorrow_intent": "string" -} -``` - -## Weekly History Entry Schema (written by Phase 8) - -```json -{ - "id": "unique-id", - "week_of": "YYYY-MM-DD (Monday)", - "run_date": "YYYY-MM-DD", - "run_at": "ISO datetime", - "review": { - "reflection": "string", - "daily_logs_count": 0, - "patterns": ["string"] - }, - "calendar": { - "events_existing": 0, - "events_created": 0, - "conflicts_resolved": 0, - "deep_work_blocks": 0 - }, - "groom": { - "drift_score_before": 0, - "drift_score_after": 0, - "stale_areas_resolved": [], - "projects_touched": 0 - }, - "priorities": [ - { - "outcome": "string", - "aligned_to": "project_id or north_star", - "status": "pending" - } - ], - "backlog": { - "items_reviewed": 0, - "graduated": 0, - "deferred": 0, - "killed": 0, - "new_captured": 0 - }, - "domains": {}, - "blockers": { - "identified": 0, - "follow_ups_queued": 0, - "resolved_since_last": 0 - }, - "changeover": { - "priorities_carried_over": 0, - "priorities_completed": 0, - "priorities_dropped": 0, - "drift_delta": 0, - "backlog_net": 0 - } -} -``` - - - - -Weekly entry logged to weekly.json. Calendar events created. Workspace groomed. Priorities locked. Backlog current. Operator walks away with a clear week ahead. - - - -- [ ] Weekly.json loaded or created on first run -- [ ] Phase 1: Daily logs consumed (if available), reflection captured -- [ ] Phase 2: Calendar pulled and reviewed, additions collected -- [ ] Phase 3: Groom executed inline, drift score updated -- [ ] Phase 4: 3-5 outcome-based priorities set, aligned to north star -- [ ] Phase 5: Overdue backlog items surfaced and processed -- [ ] Phase 6: Domain phases executed (if configured), results captured -- [ ] Phase 7: Blockers identified, follow-ups queued -- [ ] Phase 8: Summary confirmed, calendar events created with rules applied, weekly entry logged -- [ ] Each phase skippable without breaking the flow -- [ ] Operator confirmed at each decision point (voice-friendly pacing) - diff --git a/src/framework/templates/claudemd-template.md b/src/framework/templates/claudemd-template.md deleted file mode 100644 index 075c28a..0000000 --- a/src/framework/templates/claudemd-template.md +++ /dev/null @@ -1,102 +0,0 @@ -# CLAUDE.md Template - -Reference template for generating strategy-compliant CLAUDE.md files. Placeholders use `{PLACEHOLDER}` format. Comments use `` and must be removed in final output. - -*** - -```Markdown -# CLAUDE.md - -## What - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - ---- - -## Why - - - -{PHILOSOPHY} - ---- - -## Who - - - -**{WORKSPACE_NAME}** — {ONE_LINE_DESCRIPTION} - -### {BUSINESS_1_NAME} -{BUSINESS_1_URL_IF_APPLICABLE} -- {SERVICE_OR_PRODUCT_1} -- {SERVICE_OR_PRODUCT_2} -- {SERVICE_OR_PRODUCT_3} - - - - - ---- - -## Where - - - -` ` ` -{WORKSPACE_ROOT}/ -├── {DIR_1}/ # {PURPOSE} -├── {DIR_2}/ # {PURPOSE} -│ ├── {SUBDIR}/ # {PURPOSE — only if meaningful} -│ └── {SUBDIR}/ # {PURPOSE} -└── {DIR_N}/ # {PURPOSE} -` ` ` - ---- - -## How - -### Systems - - - -| System | Purpose | Location | -|--------|---------|----------| -| {SYSTEM_1} | {ONE_LINE_PURPOSE} | `{LOCATION}` | -| {SYSTEM_2} | {ONE_LINE_PURPOSE} | `{LOCATION}` | - -### Git Strategy - - - -| Directory | Approach | -|-----------|----------| -| {DIR} | {STRATEGY} | - -### Rules - - - - -NEVER {WRONG_ACTION} — {RIGHT_ACTION} -NEVER {WRONG_ACTION} — {RIGHT_ACTION} - -### Quick Reference - - - -**{ACTION}?** → {INSTRUCTION} -**{ACTION}?** → {INSTRUCTION} -``` - -*** - -## Usage Notes - -* Target: under 100 lines in final output -* Remove all `` comments before finalizing -* Remove placeholder sections that don't apply (not every workspace needs Git Strategy or Systems) -* The Where tree should reflect the ACTUAL filesystem, verified by scanning -* Rules should be workspace-identity-level, not operational. If a rule only applies during specific work (e.g., "when writing tests..."), it belongs in a domain-specific rule system, not CLAUDE.md -* `@` references point Claude to files it should read on demand — use for anything volatile or detailed - diff --git a/src/framework/templates/workspace-json.md b/src/framework/templates/workspace-json.md deleted file mode 100644 index 97688f7..0000000 --- a/src/framework/templates/workspace-json.md +++ /dev/null @@ -1,96 +0,0 @@ -# Workspace Manifest Template - -Output file: `.base/workspace.json` - -```template -{ - "workspace": "{workspace-name}", - "created": "{YYYY-MM-DD}", - "groom_cadence": "{weekly|bi-weekly|monthly}", - "groom_day": "{day-of-week}", - "areas": { - "{area-name}": { - "type": "{working-memory|directory|config-cross-ref|system-layer|custom}", - "description": "[Human-readable purpose of this area]", - "paths": ["{file-or-directory-paths}"], - "groom": "{weekly|bi-weekly|monthly}", - "audit": { - "strategy": "{staleness|classify|cross-reference|dead-code|pipeline-status}", - "config": {} - } - } - }, - "carl_hygiene": { - "proactive": true, - "cadence": "monthly", - "staleness_threshold_days": 60, - "max_rules_per_domain": 15, - "last_run": null - }, - "surfaces": { - "{surface-name}": { - "file": "data/{name}.json", - "description": "[What this surface tracks]", - "hook": true, - "silent": true, - "schema": { - "id_prefix": "{PREFIX}", - "required_fields": ["{field1}", "{field2}"], - "priority_levels": ["{level1}", "{level2}"], - "status_values": ["{status1}", "{status2}"] - } - } - }, - "satellites": { - "{project-name}": { - "path": "{relative-path-to-project}", - "engine": "{paul|custom|none}", - "state": "{path-to-state-file}", - "registered": "{YYYY-MM-DD}", - "groom_check": true, - "last_activity": null, - "phase_name": null, - "phase_number": null, - "phase_status": null, - "loop_position": "IDLE", - "handoff": false, - "last_plan_completed_at": null - } - } -} -``` - -## Field Documentation - -| Field | Type | Description | -|-------|------|------------| -| workspace | string | Name of this workspace (typically the directory name) | -| created | date | When BASE was initialized in this workspace | -| groom_cadence | enum | Default grooming frequency for the workspace | -| groom_day | string | Preferred day for weekly grooming | -| areas | object | Map of tracked workspace areas | -| areas.*.type | enum | Classification of the area for audit strategy selection | -| areas.*.paths | array | Files or directories this area tracks | -| areas.*.groom | enum | Grooming frequency for this specific area (overrides default) | -| areas.*.audit.strategy | enum | Which audit strategy to apply (see audit-strategies.md) | -| areas.*.audit.config | object | Strategy-specific configuration | -| carl_hygiene | object | CARL rule lifecycle management config (optional — only if CARL is installed) | -| carl_hygiene.proactive | boolean | Auto-surface stale rules during groom | -| carl_hygiene.cadence | enum | How often to run CARL hygiene | -| carl_hygiene.staleness_threshold_days | number | Days before a rule is flagged as stale | -| carl_hygiene.max_rules_per_domain | number | Soft cap per domain (warn, not enforce) | -| surfaces | object | Registered data surfaces with schemas | -| surfaces.*.file | string | Path to JSON file relative to .base/ | -| surfaces.*.hook | boolean | Whether a hook auto-injects this surface | -| surfaces.*.silent | boolean | Whether hook output is passive (no proactive mentions) | -| surfaces.*.schema | object | Validation schema for surface items | -| surfaces.*.schema.id_prefix | string | Auto-generated ID prefix (e.g., "ACT", "BL") | -| surfaces.*.schema.required_fields | array | Fields required on every item | -| satellites | object | External projects tracked by BASE but managed by their own engines | -| satellites.*.engine | enum | What orchestration tool manages this project | -| satellites.*.state | string | Path to the project's state file for health checks | -| satellites.*.groom_check | boolean | Whether BASE checks this project's health during groom (default: true) | -| satellites.*.last_activity | string | ISO timestamp of last project activity (synced from paul.json) | -| satellites.*.phase_name | string | Current phase name (synced from paul.json) | -| satellites.*.loop_position | string | PAUL loop state: IDLE, PLAN, APPLY, UNIFY | -| satellites.*.handoff | boolean | Whether a handoff file exists for this project | diff --git a/src/framework/utils/scan-claude-dirs.py b/src/framework/utils/scan-claude-dirs.py deleted file mode 100644 index 3b9919b..0000000 --- a/src/framework/utils/scan-claude-dirs.py +++ /dev/null @@ -1,549 +0,0 @@ -#!/usr/bin/env python3 -""" -scan-claude-dirs.py — Exhaustive .claude/ directory scanner for BASE audit-claude workflow. - -Produces a structured JSON dataset of every .claude/ directory in a workspace, -including baselines (global ~/.claude/, workspace root .claude/, MCP registry). -Every file gets an MD5 hash. No judgment, no classification — pure data collection. - -Usage: - python3 scan-claude-dirs.py [--workspace ] [--global-config ] [--output ] - -Defaults: - --workspace Current working directory - --global-config ~/.claude - --output .base/audits/data-sets/claude-scan-{date}.json - -The audit-claude workflow reads this JSON and performs classification/planning -against a complete, verified dataset instead of ad-hoc bash commands. -""" - -import argparse -import hashlib -import json -import os -from datetime import datetime, timezone - -# Directories to skip during recursive scan -SKIP_PATTERNS = { - 'node_modules', '_archive', '.git', 'vendor', 'dist', 'build', - '__pycache__', '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache' -} - - -def md5_file(filepath): - """Compute MD5 hash of a file.""" - try: - h = hashlib.md5() - with open(filepath, 'rb') as f: - for chunk in iter(lambda: f.read(8192), b''): - h.update(chunk) - return h.hexdigest() - except (OSError, PermissionError): - return None - - -def file_line_count(filepath): - """Count lines in a text file.""" - try: - with open(filepath, 'r', errors='replace') as f: - return sum(1 for _ in f) - except (OSError, PermissionError): - return None - - -def last_modified(filepath): - """Get last modification time as ISO string.""" - try: - ts = os.path.getmtime(filepath) - return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() - except OSError: - return None - - -def dir_last_modified(dirpath): - """Get the most recent modification time of any file in a directory tree.""" - latest = 0 - try: - for root, dirs, files in os.walk(dirpath): - for f in files: - fp = os.path.join(root, f) - try: - mt = os.path.getmtime(fp) - if mt > latest: - latest = mt - except OSError: - pass - except OSError: - pass - if latest == 0: - return None - return datetime.fromtimestamp(latest, tz=timezone.utc).isoformat() - - -def scan_files_in_dir(dirpath, relative_to=None): - """List all files in a directory (non-recursive) with metadata.""" - results = [] - if not os.path.isdir(dirpath): - return results - try: - for name in sorted(os.listdir(dirpath)): - fp = os.path.join(dirpath, name) - if os.path.isfile(fp): - entry = { - 'name': name, - 'md5': md5_file(fp), - 'lines': file_line_count(fp), - 'size_bytes': os.path.getsize(fp), - 'last_modified': last_modified(fp) - } - if relative_to: - entry['relative_path'] = os.path.relpath(fp, relative_to) - results.append(entry) - except (OSError, PermissionError): - pass - return results - - -def scan_files_recursive(dirpath, relative_to=None): - """List all files in a directory recursively with metadata.""" - results = [] - if not os.path.isdir(dirpath): - return results - try: - for root, dirs, files in os.walk(dirpath): - # Skip hidden dirs and known noise - dirs[:] = [d for d in dirs if d not in SKIP_PATTERNS and not d.startswith('.')] - for name in sorted(files): - fp = os.path.join(root, name) - if os.path.isfile(fp): - entry = { - 'name': name, - 'md5': md5_file(fp), - 'lines': file_line_count(fp), - 'size_bytes': os.path.getsize(fp), - 'last_modified': last_modified(fp) - } - if relative_to: - entry['relative_path'] = os.path.relpath(fp, relative_to) - results.append(entry) - except (OSError, PermissionError): - pass - return results - - -def scan_skill_dirs(skills_path): - """List skill directories with SKILL.md hash if present.""" - results = [] - if not os.path.isdir(skills_path): - return results - try: - for name in sorted(os.listdir(skills_path)): - skill_dir = os.path.join(skills_path, name) - if os.path.isdir(skill_dir): - entry = { - 'name': name, - 'file_count': sum(1 for _, _, fs in os.walk(skill_dir) for _ in fs), - 'last_modified': dir_last_modified(skill_dir) - } - skill_md = os.path.join(skill_dir, 'SKILL.md') - if os.path.isfile(skill_md): - entry['skill_md_md5'] = md5_file(skill_md) - results.append(entry) - except (OSError, PermissionError): - pass - return results - - -def scan_command_dirs(commands_path, relative_to=None): - """List all command .md files recursively.""" - results = [] - if not os.path.isdir(commands_path): - return results - try: - for root, dirs, files in os.walk(commands_path): - dirs[:] = [d for d in dirs if not d.startswith('.')] - for name in sorted(files): - if name.endswith('.md'): - fp = os.path.join(root, name) - rel = os.path.relpath(fp, commands_path) - entry = { - 'name': rel, - 'md5': md5_file(fp), - 'lines': file_line_count(fp), - 'last_modified': last_modified(fp) - } - results.append(entry) - except (OSError, PermissionError): - pass - return results - - -def parse_settings_json(filepath): - """Parse a settings.json and extract structured data.""" - if not os.path.isfile(filepath): - return None - try: - with open(filepath, 'r') as f: - data = json.load(f) - except (json.JSONDecodeError, OSError): - return {'parse_error': True, 'raw_exists': True} - - result = { - 'exists': True, - 'md5': md5_file(filepath), - 'last_modified': last_modified(filepath) - } - - # Extract hooks - hooks = data.get('hooks', {}) - hook_summary = {} - for event, entries in hooks.items(): - commands = [] - if isinstance(entries, list): - for entry in entries: - if isinstance(entry, dict): - for h in entry.get('hooks', []): - cmd = h.get('command', '') - if cmd: - commands.append(cmd) - elif isinstance(entry, str): - commands.append(entry) - hook_summary[event] = { - 'count': len(commands), - 'commands': commands, - 'is_empty_array': isinstance(entries, list) and len(entries) == 0 - } - result['hooks'] = hook_summary - - # Extract permissions - permissions = data.get('permissions', {}) - result['permissions'] = { - 'allow': permissions.get('allow', []), - 'deny': permissions.get('deny', []) - } - - # Extract MCP servers if present - mcp = data.get('mcpServers', {}) - if mcp: - result['mcp_servers'] = list(mcp.keys()) - - # Extract enabled MCP servers (settings.local.json pattern) - enabled = data.get('enabledMcpjsonServers', []) - if enabled: - result['enabled_mcp_servers'] = enabled - - enable_all = data.get('enableAllProjectMcpServers') - if enable_all is not None: - result['enable_all_project_mcp'] = enable_all - - # Project metadata - project = data.get('project', {}) - if project: - result['project'] = project - - return result - - -def parse_mcp_json(filepath): - """Parse .mcp.json and list all registered server names.""" - if not os.path.isfile(filepath): - return [] - try: - with open(filepath, 'r') as f: - data = json.load(f) - return sorted(data.get('mcpServers', {}).keys()) - except (json.JSONDecodeError, OSError): - return [] - - -def find_git_root(directory): - """Walk up from directory to find the nearest .git root. Returns path or None.""" - current = os.path.abspath(directory) - while True: - if os.path.isdir(os.path.join(current, '.git')): - return current - parent = os.path.dirname(current) - if parent == current: - return None - current = parent - - -def detect_git_boundary(claude_dir, workspace_root): - """Determine which config layers are visible when Claude Code boots in this directory. - - Claude Code resolves the project root from the nearest .git boundary. - - Global ~/.claude/ is always visible - - Workspace root .claude/ is only visible if the project's git root IS the workspace root - - The project's own .claude/ is visible if it's at or under the git root - - Returns a dict describing the visibility context. - """ - # The .claude dir's parent is where Claude Code would boot - parent_dir = os.path.dirname(claude_dir) - git_root = find_git_root(parent_dir) - - ws_abs = os.path.abspath(workspace_root) - has_own_git = git_root is not None and os.path.abspath(git_root) != ws_abs - git_root_rel = os.path.relpath(git_root, ws_abs) if git_root else None - - return { - 'has_own_git': has_own_git, - 'git_root': git_root_rel, - 'sees_global': True, # ~/.claude/ is always visible - 'sees_workspace_root': not has_own_git, # Only if git root == workspace root - 'sees_own_claude': True, # The project's .claude/ is always visible to itself - 'mcp_json_visible': not has_own_git # .mcp.json at workspace root only visible if same git root - } - - -def find_claude_dirs(workspace_root): - """Find all .claude directories recursively, respecting skip patterns.""" - results = [] - seen = set() - for root, dirs, _files in os.walk(workspace_root): - # Filter out skip patterns - dirs[:] = [d for d in dirs if d not in SKIP_PATTERNS] - - if '.claude' in dirs: - claude_path = os.path.join(root, '.claude') - real_path = os.path.realpath(claude_path) - if real_path in seen: - continue - seen.add(real_path) - - rel_path = os.path.relpath(claude_path, workspace_root) - results.append({ - 'absolute_path': claude_path, - 'relative_path': rel_path, - 'parent_dir': os.path.relpath(root, workspace_root) - }) - # Also scan inside .claude for nested .claude dirs (accidental) - nested_claude = os.path.join(claude_path, '.claude') - if os.path.isdir(nested_claude): - nested_real = os.path.realpath(nested_claude) - if nested_real not in seen: - seen.add(nested_real) - nested_rel = os.path.relpath(nested_claude, workspace_root) - results.append({ - 'absolute_path': nested_claude, - 'relative_path': nested_rel, - 'parent_dir': os.path.relpath(claude_path, workspace_root), - 'nested': True - }) - - return results - - -def scan_claude_dir(claude_dir_info, workspace_root): - """Fully scan a single .claude directory.""" - abs_path = claude_dir_info['absolute_path'] - rel_path = claude_dir_info['relative_path'] - - # Git boundary detection - git_context = detect_git_boundary(abs_path, workspace_root) - - entry = { - 'path': rel_path, - 'absolute_path': abs_path, - 'parent': claude_dir_info['parent_dir'], - 'nested': claude_dir_info.get('nested', False), - 'last_modified': dir_last_modified(abs_path), - 'is_template': any(t in rel_path for t in ['_template/', 'template/', 'templates/']), - 'git_boundary': git_context - } - - # Hooks - hooks_dir = os.path.join(abs_path, 'hooks') - entry['hooks'] = scan_files_in_dir(hooks_dir) - - # Commands (recursive — commands can have subdirs) - commands_dir = os.path.join(abs_path, 'commands') - entry['commands'] = scan_command_dirs(commands_dir) - - # Skills - skills_dir = os.path.join(abs_path, 'skills') - entry['skills'] = scan_skill_dirs(skills_dir) - - # Rules - rules_dir = os.path.join(abs_path, 'rules') - entry['rules'] = scan_files_in_dir(rules_dir) - - # Settings - entry['settings_json'] = parse_settings_json(os.path.join(abs_path, 'settings.json')) - entry['settings_local_json'] = parse_settings_json(os.path.join(abs_path, 'settings.local.json')) - - # Other files (top-level only, not in known subdirs) - known_subdirs = {'hooks', 'commands', 'skills', 'rules', 'session-context', 'worktrees'} - other = [] - try: - for name in sorted(os.listdir(abs_path)): - fp = os.path.join(abs_path, name) - if os.path.isfile(fp) and name not in ('settings.json', 'settings.local.json'): - other.append({ - 'name': name, - 'md5': md5_file(fp), - 'size_bytes': os.path.getsize(fp) - }) - elif os.path.isdir(fp) and name not in known_subdirs and name != '.claude': - other.append({ - 'name': name + '/', - 'type': 'directory', - 'file_count': sum(1 for _, _, fs in os.walk(fp) for _ in fs) - }) - except (OSError, PermissionError): - pass - entry['other'] = other - - # Subdirectory presence flags - entry['has_hooks'] = os.path.isdir(hooks_dir) and len(entry['hooks']) > 0 - entry['has_commands'] = os.path.isdir(commands_dir) and len(entry['commands']) > 0 - entry['has_skills'] = os.path.isdir(skills_dir) and len(entry['skills']) > 0 - entry['has_rules'] = os.path.isdir(rules_dir) and len(entry['rules']) > 0 - entry['has_settings'] = entry['settings_json'] is not None - entry['has_settings_local'] = entry['settings_local_json'] is not None - - # Empty directory check - total_items = ( - len(entry['hooks']) + len(entry['commands']) + len(entry['skills']) + - len(entry['rules']) + len(entry['other']) + - (1 if entry['has_settings'] else 0) + - (1 if entry['has_settings_local'] else 0) - ) - entry['is_empty'] = total_items == 0 - - return entry - - -def build_baseline(config_path, label): - """Build a baseline inventory of a .claude config directory.""" - baseline = { - 'path': config_path, - 'label': label, - 'exists': os.path.isdir(config_path) - } - - if not baseline['exists']: - return baseline - - baseline['hooks'] = scan_files_in_dir(os.path.join(config_path, 'hooks')) - baseline['commands'] = scan_command_dirs(os.path.join(config_path, 'commands')) - baseline['skills'] = scan_skill_dirs(os.path.join(config_path, 'skills')) - baseline['settings_json'] = parse_settings_json(os.path.join(config_path, 'settings.json')) - baseline['settings_local_json'] = parse_settings_json(os.path.join(config_path, 'settings.local.json')) - - # Build lookup indexes for fast comparison - baseline['hook_md5_index'] = {h['md5']: h['name'] for h in baseline['hooks'] if h['md5']} - baseline['hook_name_index'] = {h['name']: h['md5'] for h in baseline['hooks'] if h['md5']} - baseline['command_md5_index'] = {c['md5']: c['name'] for c in baseline['commands'] if c['md5']} - baseline['command_name_index'] = {c['name']: c['md5'] for c in baseline['commands'] if c['md5']} - baseline['skill_name_index'] = {s['name']: s.get('skill_md_md5') for s in baseline['skills']} - - return baseline - - -def main(): - parser = argparse.ArgumentParser(description='Scan all .claude/ directories in a workspace') - parser.add_argument('--workspace', default=os.getcwd(), help='Workspace root path') - parser.add_argument('--global-config', default=os.path.join(os.path.expanduser('~'), '.claude'), - help='Global Claude config path') - parser.add_argument('--output', default=None, help='Output JSON path') - args = parser.parse_args() - - workspace = os.path.abspath(args.workspace) - global_config = os.path.abspath(args.global_config) - - # Default output path - if args.output: - output_path = os.path.abspath(args.output) - else: - datasets_dir = os.path.join(workspace, '.base', 'audits', 'data-sets') - os.makedirs(datasets_dir, exist_ok=True) - date_str = datetime.now().strftime('%Y-%m-%d') - output_path = os.path.join(datasets_dir, f'claude-scan-{date_str}.json') - - # Ensure output directory exists - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - # Build baselines - global_baseline = build_baseline(global_config, 'global') - workspace_root_claude = os.path.join(workspace, '.claude') - workspace_baseline = build_baseline(workspace_root_claude, 'workspace_root') - - # MCP registry - mcp_servers = parse_mcp_json(os.path.join(workspace, '.mcp.json')) - - # Discover all .claude directories - all_claude_dirs = find_claude_dirs(workspace) - - # Separate root from project-level - project_dirs = [ - d for d in all_claude_dirs - if d['relative_path'] != '.claude' - ] - - # Scan each project-level .claude directory - scanned_directories = [] - for dir_info in project_dirs: - scanned = scan_claude_dir(dir_info, workspace) - scanned_directories.append(scanned) - - # Build the complete dataset - dataset = { - 'meta': { - 'scan_date': datetime.now(tz=timezone.utc).isoformat(), - 'workspace': workspace, - 'global_config': global_config, - 'scanner_version': '1.1.0', - 'total_directories_found': len(all_claude_dirs), - 'project_directories_scanned': len(project_dirs), - 'baseline_directories': 2 - }, - 'baselines': { - 'global': global_baseline, - 'workspace_root': workspace_baseline, - 'mcp_registry': mcp_servers - }, - 'directories': scanned_directories - } - - # Counts summary - total_hooks = sum(len(d['hooks']) for d in scanned_directories) - total_commands = sum(len(d['commands']) for d in scanned_directories) - total_skills = sum(len(d['skills']) for d in scanned_directories) - total_settings = sum(1 for d in scanned_directories if d['has_settings']) - total_settings_local = sum(1 for d in scanned_directories if d['has_settings_local']) - nested_count = sum(1 for d in scanned_directories if d['nested']) - empty_count = sum(1 for d in scanned_directories if d['is_empty']) - template_count = sum(1 for d in scanned_directories if d['is_template']) - own_git_count = sum(1 for d in scanned_directories if d.get('git_boundary', {}).get('has_own_git', False)) - inherits_workspace_count = sum(1 for d in scanned_directories if not d.get('git_boundary', {}).get('has_own_git', False)) - - dataset['summary'] = { - 'total_project_claude_dirs': len(project_dirs), - 'total_hooks': total_hooks, - 'total_commands': total_commands, - 'total_skills': total_skills, - 'total_settings_json': total_settings, - 'total_settings_local_json': total_settings_local, - 'nested_dirs': nested_count, - 'empty_dirs': empty_count, - 'template_dirs': template_count, - 'own_git_boundary': own_git_count, - 'inherits_workspace_root': inherits_workspace_count - } - - # Write output - with open(output_path, 'w') as f: - json.dump(dataset, f, indent=2) - - # Print summary to stdout for the hook/task to capture - print(json.dumps({ - 'status': 'complete', - 'output': output_path, - 'summary': dataset['summary'] - })) - - -if __name__ == '__main__': - main() diff --git a/src/hooks/_template.py b/src/hooks/_template.py deleted file mode 100644 index 802e863..0000000 --- a/src/hooks/_template.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -""" -BASE Hook Template — Canonical reference for data surface injection hooks. - -THIS IS A TEMPLATE, NOT A RUNNABLE HOOK. -Copy this file, rename it to {surface}-hook.py, and customize the marked sections. - -=== CONTRACT === -Every data surface hook MUST follow this contract: - 1. Reads ONE JSON file from .base/data/{SURFACE_NAME}.json - 2. Outputs a compact XML-tagged block to stdout - 3. Wraps output in <{SURFACE_NAME}-awareness> tags - 4. Includes a BEHAVIOR directive block - 5. Exits cleanly (exit 0) — never crashes, never blocks - -=== DO === - - Read the JSON file using absolute paths (Path(__file__).resolve()) - - Format a compact summary: IDs, one-line descriptions, grouped by priority/status - - Include item count summaries - - Include the behavioral directive (passive by default) - - Handle missing/empty/malformed files gracefully (output nothing, exit 0) - - Keep output compact — hooks fire every prompt, token cost matters - -=== DO NOT === - - Never write to any file - - Never make network calls - - Never import heavy dependencies (sys, json, pathlib ONLY) - - Never read multiple data files (one hook = one surface) - - Never include full item details in injection (that's what MCP tools are for) - - Never include dynamic logic that changes based on time of day, session count, etc. - -=== TRIGGERS === -Register in .claude/settings.json under UserPromptSubmit. -Use `which python3` to detect the absolute python path for your system. - { - "type": "command", - "command": "{absolute_python3_path} /absolute/path/to/.base/hooks/{surface}-hook.py" - } -""" - -import sys -import json -from pathlib import Path - -# ============================================================ -# CONFIGURATION — CUSTOMIZE THIS -# ============================================================ - -SURFACE_NAME = "example" # CHANGE THIS: your surface name (e.g., "active", "backlog") - -# ============================================================ -# PATH RESOLUTION — DO NOT CHANGE -# ============================================================ - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent # .base/hooks/ → .base/ → workspace root is parent of .base/ -# Fix: .base/hooks/_template.py → .base/ is parent, workspace root is parent of .base/ -WORKSPACE_ROOT = HOOK_DIR.parent.parent -DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / f"{SURFACE_NAME}.json" - -# ============================================================ -# BEHAVIORAL DIRECTIVE — CUSTOMIZE IF NEEDED -# ============================================================ - -BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. -Do NOT proactively mention these items unless: - - User explicitly asks (e.g., "what should I work on?", "what's next?") - - A deadline is within 24 hours AND user hasn't acknowledged it this session -For details on any item, use base_get_item("{SURFACE_NAME}", id).""" - - -def main(): - # --- Read hook input from stdin (Claude Code provides session context) --- - try: - input_data = json.loads(sys.stdin.read()) - session_id = input_data.get("session_id", "") - except (json.JSONDecodeError, OSError): - session_id = "" - - # --- Guard: file must exist --- - if not DATA_FILE.exists(): - sys.exit(0) - - # --- Read and parse JSON --- - try: - data = json.loads(DATA_FILE.read_text()) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - # ============================================================ - # ITEM EXTRACTION — CUSTOMIZE THIS - # ============================================================ - # Default expects: { "items": [ { "id": "...", "title": "...", ... }, ... ] } - # Adjust the key and field names to match your surface's schema. - - items = data.get("items", []) - - if not items: - sys.exit(0) - - # ============================================================ - # SUMMARY FORMATTING — CUSTOMIZE THIS - # ============================================================ - # Build compact summary lines. Keep it SHORT — one line per item max. - # Group by status/priority if your schema supports it. - # Example format: "- [ID] Title (status)" - - lines = [] - for item in items: - item_id = item.get("id", "?") - title = item.get("title", "untitled") - status = item.get("status", "") - status_suffix = f" ({status})" if status else "" - lines.append(f"- [{item_id}] {title}{status_suffix}") - - # --- Output --- - if lines: - count = len(items) - summary = "\n".join(lines) - print(f"""<{SURFACE_NAME}-awareness items="{count}"> -{summary} - -{BEHAVIOR_DIRECTIVE} -""") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/active-hook.py b/src/hooks/active-hook.py deleted file mode 100644 index cca4716..0000000 --- a/src/hooks/active-hook.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3 -""" -BASE Hook v2: active-hook-v2.py -Source: .base/data/projects.json (APEX unified project management) -Output: compact summary grouped by priority -Filters: items with status NOT in [backlog, archived] - -Drop-in replacement for active-hook.py. Swap in settings.json when ready. -Legacy active-hook.py reads from .base/data/active.json (unchanged). -""" - -import sys -import json -from pathlib import Path -from datetime import date, datetime - -SURFACE_NAME = "active" - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent -DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / "projects.json" - -BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. -Do NOT proactively mention these items unless: - - User explicitly asks (e.g., "what should I work on?", "what's next?") - - A deadline is within 24 hours AND user hasn't acknowledged it this session -For details on any item, use base_get_project(id).""" - -PRIORITY_ORDER = ["urgent", "high", "medium", "low", "ongoing", "deferred"] - -# Staleness thresholds (days since last update) -STALE_THRESHOLDS = { - "urgent": 3, - "high": 5, - "medium": 7, - "low": 14, - "ongoing": 14, - "deferred": 30, -} - -# Statuses that are NOT active (excluded from active view) -EXCLUDED_STATUSES = {"backlog", "archived", "completed"} - -# Types to exclude from active awareness (checked via MCP during grooms) -EXCLUDED_TYPES = {"initiative"} - - -def days_since_update(item): - """Calculate days since last update. Uses updated_at (ISO datetime).""" - ts = item.get("updated_at") or item.get("created_at") - if not ts: - return None - try: - d = date.fromisoformat(ts[:10]) - return (date.today() - d).days - except (ValueError, TypeError): - return None - - -def main(): - try: - input_data = json.loads(sys.stdin.read()) - except (json.JSONDecodeError, OSError): - pass - - if not DATA_FILE.exists(): - sys.exit(0) - - try: - data = json.loads(DATA_FILE.read_text()) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - items = data.get("items", []) - if not items: - sys.exit(0) - - # Filter: only active items (not backlog, archived, completed), exclude initiatives - active_items = [i for i in items if i.get("status") not in EXCLUDED_STATUSES and i.get("type") not in EXCLUDED_TYPES] - if not active_items: - sys.exit(0) - - # Group by priority - groups = {} - for item in active_items: - p = item.get("priority", "medium") - groups.setdefault(p, []).append(item) - - # Workload balance header - blocked_count = sum(1 for i in active_items if i.get("blocked_by")) - ongoing_count = sum(1 for i in active_items if i.get("priority") == "ongoing") - deferred_count = sum(1 for i in active_items if i.get("status") == "deferred") - working_count = len(active_items) - ongoing_count - deferred_count - lines = [f"Load: {working_count} active | {blocked_count} blocked | {ongoing_count} ongoing | {deferred_count} deferred"] - - for priority in PRIORITY_ORDER: - group = groups.get(priority, []) - if not group: - continue - lines.append(f"[{priority.upper()}]") - for item in group: - item_id = item.get("id", "?") - title = item.get("title", "untitled") - status = item.get("status", "") - category = item.get("category", "") - cat_tag = f"({category}) " if category else "" - parts = [f"- [{item_id}] {cat_tag}{title}"] - if status: - parts[0] += f" ({status})" - # PAUL signal (phase, loop, plan age, handoff) — only if paul data has real values - paul_info = item.get("paul") - if paul_info and paul_info.get("is_paul_project") and paul_info.get("phase"): - paul_parts = [] - p_phase = paul_info.get("phase", "?") - p_completed = paul_info.get("completed_phases", "?") - p_total = paul_info.get("total_phases", "?") - p_loop = paul_info.get("loop_position", "?") - paul_parts.append(f"Phase {p_completed}/{p_total} ({p_phase})") - paul_parts.append(str(p_loop)) - # Plan age - last_plan = paul_info.get("last_plan_completed_at") or paul_info.get("last_update") - if last_plan: - try: - lp = last_plan.replace("Z", "+00:00") - if "T" in lp: - lp_date = datetime.fromisoformat(lp).date() if hasattr(datetime, 'fromisoformat') else date.fromisoformat(lp[:10]) - else: - lp_date = date.fromisoformat(lp) - age = (date.today() - lp_date).days - paul_parts.append(f"plan {age}d ago") - except (ValueError, TypeError): - pass - # Handoff flag - p_handoff = paul_info.get("handoff") - if isinstance(p_handoff, dict) and p_handoff.get("present"): - paul_parts.append("HANDOFF") - elif isinstance(p_handoff, bool) and p_handoff: - paul_parts.append("HANDOFF") - parts.append(f" PAUL: {' | '.join(paul_parts)}") - - # Revenue signal - rev = item.get("revenue") - if rev and rev.get("amount"): - rev_type = rev.get("type", "") - parts.append(f" REV: {rev['amount']} ({rev_type})") - - blocked = item.get("blocked_by") - if blocked: - parts.append(f" BLOCKED: {blocked}") - next_action = item.get("next") - if next_action and priority != "ongoing": - parts.append(f" NEXT: {next_action}") - deadline = item.get("due_date") - if deadline: - parts.append(f" DUE: {deadline}") - days = days_since_update(item) - threshold = STALE_THRESHOLDS.get(priority, 7) - if days is not None: - if days >= threshold: - parts.append(f" STALE: {days}d since update (threshold: {threshold}d)") - else: - parts.append(f" updated: {days}d ago") - lines.append("\n".join(parts)) - - if lines: - count = len(active_items) - summary = "\n".join(lines) - print(f"""<{SURFACE_NAME}-awareness items="{count}"> -{summary} - -{BEHAVIOR_DIRECTIVE} -""") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/apex-insights.py b/src/hooks/apex-insights.py deleted file mode 100644 index 9016734..0000000 --- a/src/hooks/apex-insights.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -""" -APEX Insights — On-demand workspace analytics -Computes velocity, stall detection, blocking analysis, workload, and dependency chains. -Invoked by /apex:insights slash command via !command injection. -""" - -import json -import sys -from datetime import datetime, date -from pathlib import Path -from collections import defaultdict - -WORKSPACE = Path(__file__).resolve().parent.parent.parent -PROJECTS_FILE = WORKSPACE / ".base" / "data" / "projects.json" -WORKSPACE_JSON = WORKSPACE / ".base" / "workspace.json" - - -def load_json(path): - try: - with open(path) as f: - return json.load(f) - except (json.JSONDecodeError, OSError): - return None - - -def days_ago(iso_str): - if not iso_str: - return None - try: - s = iso_str.replace("Z", "+00:00") - if "T" in s: - d = datetime.fromisoformat(s).date() - else: - d = date.fromisoformat(s[:10]) - return (date.today() - d).days - except (ValueError, TypeError): - return None - - -def main(): - projects = load_json(PROJECTS_FILE) - workspace = load_json(WORKSPACE_JSON) - - if not projects: - print("ERROR: Cannot read projects.json") - sys.exit(0) - - items = projects.get("items", []) - satellites = (workspace or {}).get("satellites", {}) - - # --- VELOCITY --- - print("## VELOCITY (PAUL Projects)") - paul_projects = [] - for item in items: - paul = item.get("paul") - if paul and paul.get("is_paul_project") and paul.get("phase"): - lp_age = days_ago(paul.get("last_plan_completed_at") or paul.get("last_update")) - paul_projects.append({ - "id": item["id"], - "title": item["title"][:35], - "phase": f"{paul.get('completed_phases', '?')}/{paul.get('total_phases', '?')}", - "loop": paul.get("loop_position", "?"), - "last_plan_age": lp_age, - "handoff": paul.get("handoff", False), - "status": item.get("status"), - }) - - if paul_projects: - for p in sorted(paul_projects, key=lambda x: (x["last_plan_age"] or 0), reverse=True): - age_str = f"{p['last_plan_age']}d ago" if p["last_plan_age"] is not None else "never" - hf = " [HANDOFF]" if (isinstance(p["handoff"], dict) and p["handoff"].get("present")) or p["handoff"] is True else "" - print(f" {p['id']} {p['title']:35s} Phase {p['phase']:8s} {p['loop']:5s} plan: {age_str}{hf}") - else: - print(" No PAUL projects found") - print() - - # --- STALLS (active projects with plan age > 14d) --- - print("## STALLS (plan age > 14 days, not completed/deferred)") - stalls = [p for p in paul_projects - if p["last_plan_age"] is not None - and p["last_plan_age"] > 14 - and p["status"] not in ("completed", "deferred", "archived")] - if stalls: - for s in sorted(stalls, key=lambda x: x["last_plan_age"], reverse=True): - print(f" {s['id']} {s['title']:35s} STALLED {s['last_plan_age']}d") - else: - print(" No stalls detected") - print() - - # --- BLOCKING ANALYSIS --- - print("## BLOCKING ANALYSIS") - blocked = [i for i in items if i.get("blocked_by") and i.get("status") not in ("completed", "archived")] - if blocked: - # Group by blocker - blockers = defaultdict(list) - for item in blocked: - blockers[item["blocked_by"]].append(item) - - for blocker, items_blocked in blockers.items(): - rev_items = [i for i in items_blocked if i.get("revenue")] - rev_str = "" - if rev_items: - rev_str = f" | Revenue at risk: {', '.join(i['revenue']['amount'] for i in rev_items)}" - print(f" Blocker: {blocker}") - for i in items_blocked: - print(f" {i['id']} {i['title'][:40]}") - if rev_str: - print(f" {rev_str}") - print() - else: - print(" No blocked projects") - print() - - # --- DEPENDENCIES --- - print("## CROSS-PROJECT DEPENDENCIES") - has_deps = [i for i in items if i.get("dependencies")] - if has_deps: - for item in has_deps: - for dep in item["dependencies"]: - dep_project = next((i for i in items if i["id"] == dep["project_id"]), None) - dep_title = dep_project["title"][:30] if dep_project else dep["project_id"] - print(f" {item['id']} {item['title'][:30]} --{dep['type']}--> {dep_title}") - if dep.get("notes"): - print(f" Note: {dep['notes']}") - else: - print(" No cross-project dependencies defined") - print() - - # --- WORKLOAD BY CATEGORY --- - print("## WORKLOAD BY CATEGORY") - active = [i for i in items if i.get("status") not in ("backlog", "archived", "completed") and i.get("type") != "initiative"] - cats = defaultdict(int) - for item in active: - cats[item.get("category", "uncategorized")] += 1 - for cat, count in sorted(cats.items(), key=lambda x: -x[1]): - print(f" {cat}: {count} projects") - print() - - # --- REVENUE SUMMARY --- - print("## REVENUE EXPOSURE") - rev_projects = [i for i in items if i.get("revenue") and i.get("status") not in ("completed", "archived")] - if rev_projects: - for item in rev_projects: - rev = item["revenue"] - status = item.get("status", "?") - blocked_flag = " [BLOCKED]" if item.get("blocked_by") else "" - print(f" {item['id']} {item['title'][:35]} | {rev['amount']} ({rev['type']}){blocked_flag}") - else: - print(" No revenue projects active") - print() - - # --- HANDOFFS --- - print("## PENDING HANDOFFS") - handoff_sats = [(name, sat) for name, sat in satellites.items() if sat.get("handoff")] - if handoff_sats: - for name, sat in handoff_sats: - phase = sat.get("phase_name", "?") - print(f" {name}: Phase {phase} — has HANDOFF waiting") - else: - print(" No pending handoffs") - - -if __name__ == "__main__": - try: - main() - except Exception as e: - print(f"ERROR: {e}") - sys.exit(0) diff --git a/src/hooks/backlog-hook.py b/src/hooks/backlog-hook.py deleted file mode 100644 index 1f4599d..0000000 --- a/src/hooks/backlog-hook.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -""" -BASE Hook v2: backlog-hook-v2.py -Source: .base/data/projects.json (APEX unified project management) -Output: compact summary grouped by priority -Filters: only items with status "backlog" - -Drop-in replacement for backlog-hook.py. Swap in settings.json when ready. -Legacy backlog-hook.py reads from .base/data/backlog.json (unchanged). -""" - -import sys -import json -from pathlib import Path -from datetime import date - -SURFACE_NAME = "backlog" - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent -DATA_FILE = WORKSPACE_ROOT / ".base" / "data" / "projects.json" - -BEHAVIOR_DIRECTIVE = f"""BEHAVIOR: This context is PASSIVE AWARENESS ONLY. -Do NOT proactively mention these items unless: - - User explicitly asks (e.g., "what's in the backlog?", "what's queued?") - - A review_by date has passed AND user hasn't acknowledged it this session -For details on any item, use base_get_project(id).""" - -PRIORITY_ORDER = ["high", "medium", "low"] - -# Staleness thresholds (days since last update) -STALE_THRESHOLDS = { - "high": 7, - "medium": 14, - "low": 30, -} - - -def days_since_update(item): - """Calculate days since last update. Uses updated_at (ISO datetime).""" - ts = item.get("updated_at") or item.get("created_at") - if not ts: - return None - try: - d = date.fromisoformat(ts[:10]) - return (date.today() - d).days - except (ValueError, TypeError): - return None - - -def main(): - try: - input_data = json.loads(sys.stdin.read()) - except (json.JSONDecodeError, OSError): - pass - - if not DATA_FILE.exists(): - sys.exit(0) - - try: - data = json.loads(DATA_FILE.read_text()) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - items = data.get("items", []) - if not items: - sys.exit(0) - - # Filter: only backlog items - backlog_items = [i for i in items if i.get("status") == "backlog"] - if not backlog_items: - sys.exit(0) - - # Group by priority - groups = {} - for item in backlog_items: - p = item.get("priority", "medium") - groups.setdefault(p, []).append(item) - - lines = [] - for priority in PRIORITY_ORDER: - group = groups.get(priority, []) - if not group: - continue - lines.append(f"[{priority.upper()}]") - for item in group: - item_id = item.get("id", "?") - title = item.get("title", "untitled") - review_by = item.get("review_by") - entry = f"- [{item_id}] {title}" - if review_by: - entry += f" [review by: {review_by}]" - days = days_since_update(item) - threshold = STALE_THRESHOLDS.get(priority, 14) - if days is not None: - if days >= threshold: - entry += f" STALE: {days}d" - else: - entry += f" ({days}d ago)" - lines.append(entry) - - if lines: - count = len(backlog_items) - summary = "\n".join(lines) - print(f"""<{SURFACE_NAME}-awareness items="{count}"> -{summary} - -{BEHAVIOR_DIRECTIVE} -""") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/base-pulse-check.py b/src/hooks/base-pulse-check.py deleted file mode 100644 index ef4db98..0000000 --- a/src/hooks/base-pulse-check.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -""" -BASE Hook v2: base-pulse-check-v2.py -Purpose: Workspace health check on session start. - Reads .base/data/state.json (pre-calculated drift, areas, groom config). - Much simpler than v1 which parsed STATE.md text + computed drift from file mtimes. -Triggers: UserPromptSubmit (session context) -Output: workspace health status or groom reminder - -Drop-in replacement for base-pulse-check.py. Swap in settings.json when ready. -Legacy base-pulse-check.py reads STATE.md + workspace.json (unchanged). -""" - -import sys -import json -from datetime import datetime, date -from pathlib import Path - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent -BASE_DIR = WORKSPACE_ROOT / ".base" -STATE_FILE = BASE_DIR / "data" / "state.json" -PROJECTS_FILE = BASE_DIR / "data" / "projects.json" -CARL_DIR = WORKSPACE_ROOT / ".carl" -CARL_JSON = CARL_DIR / "carl.json" - - -def recalculate_drift(state): - """Recalculate drift indicators from live data and update state.json. - - This ensures drift score is always fresh on session start, not stale - from the last time base_update_drift was manually called. - """ - now = date.today() - - # Calculate indicators from projects.json - indicators = { - "active_age_days": 0, - "backlog_age_days": 0, - "backlog_past_review": 0, - "orphaned_sessions": 0, - "untracked_root_files": 0, - "stale_satellites": 0, - } - - if PROJECTS_FILE.exists(): - try: - projects = json.loads(PROJECTS_FILE.read_text()) - items = projects.get("items", []) - - # Active staleness: max days since update for active/in_progress/blocked/in_review projects - active_statuses = {"in_progress", "blocked", "in_review", "todo"} - active_ages = [] - backlog_ages = [] - past_review = 0 - - for item in items: - if item.get("type") != "project": - continue - - updated = item.get("updated_at") - if updated: - try: - updated_date = datetime.fromisoformat(updated).date() - age = (now - updated_date).days - except (ValueError, TypeError): - age = 0 - else: - age = 0 - - status = item.get("status", "") - if status in active_statuses: - active_ages.append(age) - elif status == "backlog": - backlog_ages.append(age) - - # Check review_by dates - review_by = item.get("review_by") - if review_by: - try: - review_date = date.fromisoformat(review_by) - if now > review_date: - past_review += 1 - except (ValueError, TypeError): - pass - - indicators["active_age_days"] = max(active_ages) if active_ages else 0 - indicators["backlog_age_days"] = max(backlog_ages) if backlog_ages else 0 - indicators["backlog_past_review"] = past_review - - except (json.JSONDecodeError, OSError): - pass - - # Stale satellites: check paul.json timestamps - satellites = state.get("satellites", {}) - stale_sats = 0 - for name, sat in satellites.items(): - sat_path = WORKSPACE_ROOT / sat.get("path", "") / ".paul" / "paul.json" - if sat_path.exists(): - try: - paul = json.loads(sat_path.read_text()) - ts = paul.get("timestamps", {}).get("updated_at") - if ts: - updated_date = datetime.fromisoformat(ts).date() - if (now - updated_date).days > 14: - stale_sats += 1 - except (json.JSONDecodeError, OSError, ValueError): - pass - indicators["stale_satellites"] = stale_sats - - # Compute score as sum of indicators - score = sum(v for v in indicators.values() if isinstance(v, (int, float))) - - # Write back to state - if "drift" not in state: - state["drift"] = {} - state["drift"]["score"] = score - state["drift"]["indicators"] = indicators - - try: - state["last_modified"] = datetime.now().isoformat() - STATE_FILE.write_text(json.dumps(state, indent=2)) - except OSError: - pass - - return state - - -def main(): - if not STATE_FILE.exists(): - sys.exit(0) - - try: - state = json.loads(STATE_FILE.read_text()) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - # Self-heal: recalculate drift from live data every session start - state = recalculate_drift(state) - - output_parts = [] - now = date.today() - - # Check groom overdue - groom = state.get("groom", {}) - next_due = groom.get("next_groom_due") - if next_due: - try: - due_date = date.fromisoformat(next_due) - if now > due_date: - last_groom = groom.get("last_groom", "unknown") - overdue_days = (now - due_date).days - output_parts.append( - f"BASE: Workspace groom overdue by {overdue_days} days " - f"(last groom: {last_groom}). " - f"Run /base:groom to maintain workspace health." - ) - except ValueError: - pass - - # Drift score and stale areas - drift = state.get("drift", {}) - drift_score = drift.get("score", 0) - areas = state.get("areas", {}) - stale_areas = [name for name, area in areas.items() if area.get("status") in ("stale", "critical")] - - if stale_areas: - output_parts.append( - f"BASE drift score: {drift_score} | Stale areas: {', '.join(stale_areas)}" - ) - elif drift_score == 0: - last_groom = groom.get("last_groom", "unknown") - output_parts.append( - f"BASE: Drift 0 | Last groom: {last_groom} | All areas current" - ) - - # CARL hygiene reminder - carl_hygiene = state.get("carl_hygiene", {}) - if carl_hygiene.get("proactive", False): - hygiene_cadence = {"weekly": 7, "bi-weekly": 14, "monthly": 30}.get( - carl_hygiene.get("cadence", "monthly"), 30 - ) - last_run = carl_hygiene.get("last_run") - if last_run: - try: - last_run_date = date.fromisoformat(last_run) - days_since = (now - last_run_date).days - if days_since > hygiene_cadence: - output_parts.append( - f"CARL hygiene overdue ({days_since}d since last run). Run /base:carl-hygiene" - ) - except ValueError: - output_parts.append("CARL hygiene: last_run date invalid. Run /base:carl-hygiene") - else: - output_parts.append("CARL hygiene never run. Run /base:carl-hygiene when ready") - - # Check staging proposals in carl.json - if CARL_JSON.exists(): - try: - carl_data = json.loads(CARL_JSON.read_text()) - pending = [p for p in carl_data.get("staging", []) if p.get("status") == "pending"] - if pending: - output_parts[-1] += f" | {len(pending)} staged proposals pending" - except (json.JSONDecodeError, OSError): - pass - - if output_parts: - print(f""" -{chr(10).join(output_parts)} -""") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/operator.py b/src/hooks/operator.py deleted file mode 100644 index 5b4aa0c..0000000 --- a/src/hooks/operator.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -""" -BASE Hook: operator.py -Source: .base/operator.json -Output: compact identity summary for alignment context -Controlled by: hook_active field in operator.json (true/false) -""" - -import json -from pathlib import Path - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent -DATA_FILE = WORKSPACE_ROOT / ".base" / "operator.json" - - -def main(): - if not DATA_FILE.exists(): - return - - try: - data = json.loads(DATA_FILE.read_text()) - except (json.JSONDecodeError, IOError): - return - - # Check activation flag - if not data.get("hook_active", False): - return - - # Extract high-signal fields - north_star = data.get("north_star", {}).get("metric", "Not set") - timeframe = data.get("north_star", {}).get("timeframe", "") - deep_why = data.get("deep_why", {}).get("statement", "Not set") - values = [v.get("value", "") for v in data.get("key_values", {}).get("values", [])] - vision = data.get("surface_vision", {}).get("summary", "Not set") - pitch = data.get("elevator_pitch", {}).get("pitch", "Not set") - - values_str = ", ".join(values) if values else "Not set" - star_str = f"{north_star} ({timeframe})" if timeframe else north_star - - output = f""" -North Star: {star_str} -Deep Why: {deep_why} -Values: {values_str} -Vision: {vision} -Pitch: {pitch} -""" - - print(output) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/psmm-injector.py b/src/hooks/psmm-injector.py deleted file mode 100644 index 331faf4..0000000 --- a/src/hooks/psmm-injector.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -""" -Hook: psmm-injector.py -Purpose: Per-Session Meta Memory — inject ephemeral session observations - into every prompt so they stay hot in long sessions (1M window). - - Uses a single psmm.json file with session-keyed entries. - Each session gets its own array keyed by Claude Code session UUID. - Stale sessions are NOT auto-cleaned — that's the operator's job - via CARL hygiene / BASE drift detection. - -Triggers: UserPromptSubmit -Output: Current session's PSMM entries as system context, or silent if empty. -""" - -import sys -import json -from pathlib import Path - -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent -PSMM_FILE = WORKSPACE_ROOT / ".base" / "data" / "psmm.json" - - -def main(): - # Get session_id from hook input - try: - input_data = json.loads(sys.stdin.read()) - session_id = input_data.get("session_id", "") - except (json.JSONDecodeError, OSError): - session_id = "" - - if not session_id or not PSMM_FILE.exists(): - sys.exit(0) - - try: - data = json.loads(PSMM_FILE.read_text()) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - sessions = data.get("sessions", {}) - session = sessions.get(session_id) - - if not session or not session.get("entries"): - sys.exit(0) - - # Build output from this session's entries - entries = session["entries"] - lines = [] - for entry in entries: - entry_type = entry.get("type", "NOTE") - text = entry.get("text", "") - timestamp = entry.get("timestamp", "") - lines.append(f"- [{timestamp}] {entry_type}: {text}") - - if lines: - created = session.get("created", "unknown") - count = len(entries) - print(f""" -{chr(10).join(lines)} -""") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/src/hooks/satellite-detection.py b/src/hooks/satellite-detection.py deleted file mode 100644 index ead4c49..0000000 --- a/src/hooks/satellite-detection.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -""" -Hook: satellite-detection.py -Purpose: Scans the workspace recursively for .paul/paul.json files, - auto-registers new satellites, and syncs paul.json state to - workspace.json and projects.json. -Triggers: SessionStart — runs once when Claude Code starts a session. -Output: block if new satellites registered, silent otherwise. - -Sync flow (paul.json → workspace.json → projects.json): - 1. Discover paul.json files across workspace - 2. Register new satellites (existing behavior) - 3. Sync paul.json state to workspace.json satellite entries - 4. Cross-check projects.json: update paul field on matching projects - Respects satellite.sync: false as opt-out for steps 3-4. -""" - -import sys -import json -from datetime import datetime -from pathlib import Path - -# Workspace root — find .base/ relative to this hook's location -HOOK_DIR = Path(__file__).resolve().parent -WORKSPACE_ROOT = HOOK_DIR.parent.parent # hooks/ -> .base/ -> workspace -BASE_DIR = WORKSPACE_ROOT / ".base" -MANIFEST_FILE = BASE_DIR / "workspace.json" -PROJECTS_FILE = BASE_DIR / "data" / "projects.json" - - -def has_hidden_component(path: Path, workspace_root: Path) -> bool: - """ - Return True if any component of path (relative to workspace_root) starts with '.', - excluding '.paul' itself (which is the expected target directory). - """ - try: - rel = path.relative_to(workspace_root) - except ValueError: - return True # Can't relativize — skip it - return any(part.startswith(".") and part != ".paul" for part in rel.parts) - - -def find_paul_json_files(workspace_root: Path) -> list[Path]: - """ - Recursively scan workspace_root for .paul/paul.json files. - Skips any path that has a hidden directory component (starts with '.'). - """ - results = [] - try: - for paul_json in workspace_root.rglob(".paul/paul.json"): - if not has_hidden_component(paul_json, workspace_root): - results.append(paul_json) - except (OSError, PermissionError): - pass - return results - - -def should_sync(paul_data: dict) -> bool: - """Check if this satellite opts into sync. Default: True.""" - satellite = paul_data.get("satellite", {}) - return satellite.get("sync", True) - - -def sync_to_workspace(satellites: dict, paul_data: dict, name: str) -> bool: - """Sync paul.json state to workspace.json satellite entry. Returns True if changed.""" - if name not in satellites: - return False - - sat = satellites[name] - changed = False - - phase = paul_data.get("phase", {}) - loop = paul_data.get("loop", {}) - handoff = paul_data.get("handoff", {}) - - updates = { - "phase_name": phase.get("name"), - "phase_number": phase.get("number"), - "phase_status": phase.get("status"), - "loop_position": loop.get("position"), - "handoff": handoff.get("present", False), - "last_plan_completed_at": paul_data.get("last_plan_completed_at"), - "next_action": paul_data.get("next_action"), - } - - for key, value in updates.items(): - if sat.get(key) != value: - sat[key] = value - changed = True - - return changed - - -def build_paul_field(paul_data: dict, name: str, sat_path: str) -> dict: - """Build a standardized paul field from paul.json data.""" - phase = paul_data.get("phase", {}) - loop = paul_data.get("loop", {}) - handoff = paul_data.get("handoff", {}) - milestone = paul_data.get("milestone", {}) - timestamps = paul_data.get("timestamps", {}) - - completed = phase.get("number", 1) if phase.get("status") == "complete" else max(0, (phase.get("number", 1) or 1) - 1) - - return { - "is_paul_project": True, - "satellite_name": name, - "location": sat_path.rstrip("/") + "/", - "milestone": milestone.get("name"), - "phase": phase.get("name"), - "phase_name": phase.get("name"), - "loop_position": loop.get("position"), - "last_update": timestamps.get("updated_at"), - "handoff": handoff.get("present", False), - "handoff_path": handoff.get("path"), - "completed_phases": completed, - "total_phases": phase.get("total"), - "last_plan_completed_at": paul_data.get("last_plan_completed_at"), - } - - -def find_project_by_path(items: list, sat_path: str): - """Find project by location path (flexible trailing slash matching).""" - normalized = sat_path.rstrip("/") - for item in items: - loc = (item.get("location") or "").rstrip("/") - if loc == normalized: - return item - return None - - -def sync_to_projects(paul_data: dict, name: str, sat_path: str, projects_data: dict) -> str: - """Sync paul.json state to matching project in projects.json. - Returns: 'updated', 'created', or 'none'.""" - items = projects_data.get("items", []) - - # Match by satellite_name first, then by path - project = None - for item in items: - paul_field = item.get("paul") - if paul_field and paul_field.get("satellite_name") == name: - project = item - break - if not project: - project = find_project_by_path(items, sat_path) - - paul_field = build_paul_field(paul_data, name, sat_path) - - if project: - # Update existing — merge paul field - if not project.get("paul"): - project["paul"] = {} - project["paul"].update(paul_field) - project["updated_at"] = datetime.now().isoformat() - return "updated" - - # Auto-create project entry - max_num = 0 - for item in items: - match = (item.get("id") or "").replace("PRJ-", "") - try: - num = int(match) - if num > max_num: - max_num = num - except ValueError: - pass - - new_id = f"PRJ-{max_num + 1:03d}" - title = paul_data.get("project", {}).get("title") or name - now = datetime.now().isoformat() - - items.append({ - "id": new_id, - "title": title, - "type": "project", - "parent_id": None, - "status": "in_progress", - "priority": "medium", - "category": "internal", - "assignees": [], - "start_date": None, - "due_date": None, - "created_at": now, - "updated_at": now, - "location": sat_path.rstrip("/") + "/", - "blocked_by": None, - "next": None, - "notes": [], - "tags": [], - "paul": paul_field, - "relations": [], - "description": None, - }) - return "created" - - -def main(): - # Skip if BASE is not installed - if not BASE_DIR.exists() or not MANIFEST_FILE.exists(): - sys.exit(0) - - try: - with open(MANIFEST_FILE, "r") as f: - manifest = json.load(f) - except (json.JSONDecodeError, OSError): - sys.exit(0) - - satellites = manifest.get("satellites", {}) - new_registrations = [] - workspace_changed = False - projects_changed = False - - # Load projects.json for cross-check (if it exists) - projects_data = None - if PROJECTS_FILE.exists(): - try: - with open(PROJECTS_FILE, "r") as f: - projects_data = json.load(f) - except (json.JSONDecodeError, OSError): - projects_data = None - - paul_files = find_paul_json_files(WORKSPACE_ROOT) - - # Collect paul data for sync pass - paul_registry = {} # name → paul_data - - for paul_json_path in paul_files: - try: - with open(paul_json_path, "r") as f: - paul_data = json.load(f) - except (json.JSONDecodeError, OSError): - continue # Malformed or unreadable — skip silently - - name = paul_data.get("name") - if not name: - continue # No name field — skip - - paul_registry[name] = paul_data - - # Read last_activity from paul.json timestamps (if present) - last_activity = paul_data.get("timestamps", {}).get("updated_at") - - if name in satellites: - # Already registered — refresh last_activity if available - if last_activity and satellites[name].get("last_activity") != last_activity: - satellites[name]["last_activity"] = last_activity - workspace_changed = True - continue - - # New satellite — derive relative path - project_dir = paul_json_path.parent.parent - try: - rel_path = str(project_dir.relative_to(WORKSPACE_ROOT)) - except ValueError: - continue # Can't relativize — skip - - # Build registration entry - entry = { - "path": rel_path, - "engine": "paul", - "state": f"{rel_path}/.paul/STATE.md", - "registered": datetime.now().strftime("%Y-%m-%d"), - "groom_check": True, - } - if last_activity: - entry["last_activity"] = last_activity - - satellites[name] = entry - new_registrations.append(name) - workspace_changed = True - - # --- Sync pass: paul.json → workspace.json + projects.json --- - for name, paul_data in paul_registry.items(): - if not should_sync(paul_data): - continue # Opt-out — skip sync - - # Sync to workspace.json - if sync_to_workspace(satellites, paul_data, name): - workspace_changed = True - - # Sync to projects.json - if projects_data: - sat_path = satellites.get(name, {}).get("path", "") - result = sync_to_projects(paul_data, name, sat_path, projects_data) - if result in ("updated", "created"): - projects_changed = True - - # Write workspace.json if changed - if workspace_changed: - try: - manifest["satellites"] = satellites - with open(MANIFEST_FILE, "w") as f: - json.dump(manifest, f, indent=2) - f.write("\n") - except OSError: - pass # Write failed — silent - - # Write projects.json if changed - if projects_changed and projects_data: - try: - projects_data["last_modified"] = datetime.now().isoformat() - with open(PROJECTS_FILE, "w") as f: - json.dump(projects_data, f, indent=2) - f.write("\n") - except OSError: - pass # Write failed — silent - - # Output only for new registrations - if new_registrations: - names_str = ", ".join(new_registrations) - n = len(new_registrations) - print(f"\nAuto-registered {n} new satellite(s): {names_str}\n") - - sys.exit(0) - - -if __name__ == "__main__": - try: - main() - except Exception: - sys.exit(0) diff --git a/src/packages/base-mcp/index.js b/src/packages/base-mcp/index.js deleted file mode 100644 index e029ad5..0000000 --- a/src/packages/base-mcp/index.js +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env node -/** - * BASE MCP — Workspace Orchestration Server - * Builder's Automated State Engine - * - * Project management, entities, state tracking, operator profile, and PSMM. - * All data stored as JSON in .base/data/. - */ - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// Tool group imports -import { TOOLS as projectTools, handleTool as handleProject } from './tools/projects.js'; -import { TOOLS as stateTools, handleTool as handleState } from './tools/state.js'; -import { TOOLS as entityTools, handleTool as handleEntity } from './tools/entities.js'; -import { TOOLS as operatorTools, handleTool as handleOperator } from './tools/operator.js'; -import { TOOLS as psmmTools, handleTool as handlePsmm } from './tools/psmm.js'; -import { TOOLS as satelliteTools, handleTool as handleSatellite } from './tools/satellite.js'; - -// ============================================================ -// CONFIGURATION -// ============================================================ - -// Resolve workspace from this file's location: base-mcp/ → .base/ → workspace root -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const WORKSPACE_PATH = path.resolve(__dirname, '../..'); - -function debugLog(...args) { - console.error('[BASE]', new Date().toISOString(), ...args); -} - -// ============================================================ -// TOOL REGISTRY -// ============================================================ - -const ALL_TOOLS = [...projectTools, ...stateTools, ...entityTools, ...operatorTools, ...psmmTools, ...satelliteTools]; - -// Build handler lookup: tool name → handler function -const TOOL_HANDLERS = {}; -for (const tool of projectTools) TOOL_HANDLERS[tool.name] = handleProject; -for (const tool of stateTools) TOOL_HANDLERS[tool.name] = handleState; -for (const tool of entityTools) TOOL_HANDLERS[tool.name] = handleEntity; -for (const tool of operatorTools) TOOL_HANDLERS[tool.name] = handleOperator; -for (const tool of psmmTools) TOOL_HANDLERS[tool.name] = handlePsmm; -for (const tool of satelliteTools) TOOL_HANDLERS[tool.name] = handleSatellite; - -// ============================================================ -// MCP SERVER -// ============================================================ - -const server = new Server({ - name: "base-mcp", - version: "2.0.0", -}, { - capabilities: { - tools: {}, - }, -}); - -debugLog('BASE MCP Server initialized'); -debugLog('Workspace:', WORKSPACE_PATH); -debugLog('Tool groups: projects (%d), state (%d), entities (%d), operator (%d), psmm (%d), satellite (%d)', - projectTools.length, stateTools.length, entityTools.length, operatorTools.length, psmmTools.length, satelliteTools.length); -debugLog('Total tools:', ALL_TOOLS.length); - -server.setRequestHandler(ListToolsRequestSchema, async () => { - debugLog('List tools request'); - return { tools: ALL_TOOLS }; -}); - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - debugLog('Call tool:', name); - - try { - const handler = TOOL_HANDLERS[name]; - if (!handler) { - throw new Error(`Unknown tool: ${name}`); - } - - const result = await handler(name, args || {}, WORKSPACE_PATH); - - if (result === null) { - throw new Error(`Tool ${name} returned null — handler mismatch`); - } - - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - isError: false, - }; - } catch (error) { - debugLog('Error:', error.message); - return { - content: [{ type: "text", text: `Error: ${error.message}` }], - isError: true, - }; - } -}); - -// ============================================================ -// RUN -// ============================================================ - -async function runServer() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("BASE MCP Server running on stdio"); -} - -try { - await runServer(); -} catch (error) { - console.error("Fatal error:", error); - process.exit(1); -} diff --git a/src/packages/base-mcp/package.json b/src/packages/base-mcp/package.json deleted file mode 100644 index f873e13..0000000 --- a/src/packages/base-mcp/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "base-mcp", - "version": "1.0.0", - "description": "BASE Surface CRUD Server - Generic operations for registered data surfaces", - "type": "module", - "main": "index.js", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0" - } -} diff --git a/src/packages/base-mcp/tools/entities.js b/src/packages/base-mcp/tools/entities.js deleted file mode 100644 index 0f9e983..0000000 --- a/src/packages/base-mcp/tools/entities.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * BASE Entities — CRUD for entities.json - * People and organizations with relational links to projects - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; -import { validateSurface } from './validate.js'; - -function debugLog(...args) { - console.error('[BASE:entities]', new Date().toISOString(), ...args); -} - -// ============================================================ -// HELPERS -// ============================================================ - -function getEntitiesPath(workspacePath) { - return join(workspacePath, '.base', 'data', 'entities.json'); -} - -function readEntities(workspacePath) { - const filepath = getEntitiesPath(workspacePath); - if (!existsSync(filepath)) { - return { version: 1, last_modified: null, entities: [] }; - } - try { - return JSON.parse(readFileSync(filepath, 'utf-8')); - } catch (error) { - debugLog('Error reading entities.json:', error.message); - return { version: 1, last_modified: null, entities: [] }; - } -} - -function writeEntities(workspacePath, data) { - const filepath = getEntitiesPath(workspacePath); - data.last_modified = new Date().toISOString(); - validateSurface('entities', data); - writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); -} - -function generateEntityId(entities) { - let max = 0; - for (const entity of entities) { - const match = (entity.id || '').match(/^ENT-(\d+)$/); - if (match) { - const num = parseInt(match[1], 10); - if (num > max) max = num; - } - } - return `ENT-${String(max + 1).padStart(3, '0')}`; -} - -function formatTimestamp() { - return new Date().toISOString(); -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_list_entities", - description: "List all entities with optional type filter (person/organization).", - inputSchema: { - type: "object", - properties: { - type: { type: "string", enum: ["person", "organization"], description: "Filter by entity type" } - }, - required: [] - } - }, - { - name: "base_add_entity", - description: "Add a new person or organization entity. Auto-generates ENT-NNN ID.", - inputSchema: { - type: "object", - properties: { - name: { type: "string", description: "Display name" }, - type: { type: "string", enum: ["person", "organization"], description: "Entity type" }, - role: { type: "string", description: "Primary role (owner, client, partner, contractor, team member, employer)" } - }, - required: ["name", "type"] - } - }, - { - name: "base_update_entity", - description: "Update an entity's fields by ID. Shallow merge — only specified fields updated.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Entity ID (e.g., 'ENT-001')" }, - data: { type: "object", description: "Fields to update" } - }, - required: ["id", "data"] - } - }, - { - name: "base_link_entity", - description: "Add a relation between an entity and a project item. Avoids duplicate relations.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Entity ID (e.g., 'ENT-001')" }, - project_id: { type: "string", description: "Project item ID (e.g., 'PRJ-003')" }, - relationship: { type: "string", description: "Relationship type (stakeholder, client, owner, partner, contractor, assignee)" } - }, - required: ["id", "project_id", "relationship"] - } - } -]; - -// ============================================================ -// TOOL HANDLERS -// ============================================================ - -function handleListEntities(args, workspacePath) { - debugLog('Listing entities'); - const data = readEntities(workspacePath); - let entities = data.entities; - - if (args.type) { - entities = entities.filter(e => e.type === args.type); - } - - return { entities, count: entities.length, total: data.entities.length }; -} - -function handleAddEntity(args, workspacePath) { - const { name, type } = args; - if (!name) throw new Error('Missing required parameter: name'); - if (!type) throw new Error('Missing required parameter: type'); - - const data = readEntities(workspacePath); - const now = formatTimestamp(); - const id = generateEntityId(data.entities); - - const entity = { - id, - name, - type, - role: args.role || null, - relations: [], - notes: [], - created_at: now, - updated_at: now - }; - - data.entities.push(entity); - writeEntities(workspacePath, data); - - debugLog(`Added ${type} ${id}: ${name}`); - return entity; -} - -function handleUpdateEntity(args, workspacePath) { - const { id, data: updateData } = args; - if (!id) throw new Error('Missing required parameter: id'); - if (!updateData) throw new Error('Missing required parameter: data'); - - const data = readEntities(workspacePath); - const index = data.entities.findIndex(e => e.id === id); - if (index === -1) { - throw new Error(`Entity "${id}" not found. Available: ${data.entities.map(e => e.id).join(', ') || 'none'}`); - } - - data.entities[index] = { - ...data.entities[index], - ...updateData, - id, // Prevent ID overwrite - updated_at: formatTimestamp() - }; - - writeEntities(workspacePath, data); - - debugLog(`Updated ${id}`); - return data.entities[index]; -} - -function handleLinkEntity(args, workspacePath) { - const { id, project_id, relationship } = args; - if (!id) throw new Error('Missing required parameter: id'); - if (!project_id) throw new Error('Missing required parameter: project_id'); - if (!relationship) throw new Error('Missing required parameter: relationship'); - - const data = readEntities(workspacePath); - const index = data.entities.findIndex(e => e.id === id); - if (index === -1) { - throw new Error(`Entity "${id}" not found`); - } - - const entity = data.entities[index]; - if (!entity.relations) entity.relations = []; - - // Check for duplicate - const exists = entity.relations.some(r => r.project_id === project_id && r.relationship === relationship); - if (exists) { - return { already_linked: true, id, project_id, relationship, message: 'Relation already exists' }; - } - - entity.relations.push({ project_id, relationship }); - entity.updated_at = formatTimestamp(); - - writeEntities(workspacePath, data); - - debugLog(`Linked ${id} → ${project_id} (${relationship})`); - return { id, project_id, relationship, relations_count: entity.relations.length }; -} - -// ============================================================ -// HANDLER DISPATCH -// ============================================================ - -export function handleTool(name, args, workspacePath) { - switch (name) { - case 'base_list_entities': - return handleListEntities(args, workspacePath); - case 'base_add_entity': - return handleAddEntity(args, workspacePath); - case 'base_update_entity': - return handleUpdateEntity(args, workspacePath); - case 'base_link_entity': - return handleLinkEntity(args, workspacePath); - default: - return null; - } -} diff --git a/src/packages/base-mcp/tools/operator.js b/src/packages/base-mcp/tools/operator.js deleted file mode 100644 index 8035411..0000000 --- a/src/packages/base-mcp/tools/operator.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Operator CRUD tools for .base/operator.json - * Read, update sections, toggle hook activation - */ - -import fs from 'fs'; -import path from 'path'; - -function debugLog(...args) { - console.error('[BASE:operator]', new Date().toISOString(), ...args); -} - -function getOperatorPath(workspacePath) { - return path.join(workspacePath, '.base', 'operator.json'); -} - -function readOperator(workspacePath) { - const filepath = getOperatorPath(workspacePath); - try { - return JSON.parse(fs.readFileSync(filepath, 'utf8')); - } catch (error) { - return null; - } -} - -function writeOperator(workspacePath, data) { - const filepath = getOperatorPath(workspacePath); - data.last_updated = new Date().toISOString(); - fs.writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n'); -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_get_operator", - description: "Read the full operator profile from .base/operator.json. Returns identity, deep why, north star, values, pitch, vision.", - inputSchema: { type: "object", properties: {}, required: [] } - }, - { - name: "base_update_operator", - description: "Update a specific section of the operator profile. Sections: deep_why, north_star, key_values, elevator_pitch, surface_vision, extensions, hook_active. Pass the full section object to replace it.", - inputSchema: { - type: "object", - properties: { - section: { type: "string", description: "Section to update (deep_why, north_star, key_values, elevator_pitch, surface_vision, extensions, hook_active)" }, - data: { description: "New section data (object for sections, boolean for hook_active)" } - }, - required: ["section", "data"] - } - } -]; - -// ============================================================ -// TOOL HANDLERS -// ============================================================ - -async function handleGetOperator(_args, workspacePath) { - const data = readOperator(workspacePath); - if (!data) { - return { error: "No operator.json found. Run /base:orientation to create one." }; - } - return data; -} - -async function handleUpdateOperator(args, workspacePath) { - const { section, data: sectionData } = args; - if (!section) throw new Error('Missing required parameter: section'); - if (sectionData === undefined) throw new Error('Missing required parameter: data'); - - const validSections = ['deep_why', 'north_star', 'key_values', 'elevator_pitch', 'surface_vision', 'extensions', 'hook_active']; - if (!validSections.includes(section)) { - throw new Error(`Invalid section "${section}". Valid: ${validSections.join(', ')}`); - } - - const operator = readOperator(workspacePath); - if (!operator) { - throw new Error('No operator.json found. Run /base:orientation to create one.'); - } - - debugLog('Updating operator section:', section); - - if (section === 'hook_active') { - operator.hook_active = !!sectionData; - } else { - operator[section] = sectionData; - } - - writeOperator(workspacePath, operator); - - return { - section, - updated: true, - hook_active: operator.hook_active - }; -} - -export async function handleTool(name, args, workspacePath) { - switch (name) { - case 'base_get_operator': return handleGetOperator(args, workspacePath); - case 'base_update_operator': return handleUpdateOperator(args, workspacePath); - default: return null; - } -} diff --git a/src/packages/base-mcp/tools/projects.js b/src/packages/base-mcp/tools/projects.js deleted file mode 100644 index 8edc91f..0000000 --- a/src/packages/base-mcp/tools/projects.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * BASE Projects — Hierarchy-aware CRUD for projects.json - * Supports Initiative > Project > Task with auto-ID by type - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; -import { validateSurface } from './validate.js'; - -function debugLog(...args) { - console.error('[BASE:projects]', new Date().toISOString(), ...args); -} - -// ============================================================ -// HELPERS -// ============================================================ - -const TYPE_PREFIX = { initiative: 'INI', project: 'PRJ', task: 'TSK' }; - -function getProjectsPath(workspacePath) { - return join(workspacePath, '.base', 'data', 'projects.json'); -} - -function readProjects(workspacePath) { - const filepath = getProjectsPath(workspacePath); - if (!existsSync(filepath)) { - return { version: 1, workspace: '', last_modified: null, categories: [], items: [], archived: [] }; - } - try { - return JSON.parse(readFileSync(filepath, 'utf-8')); - } catch (error) { - debugLog('Error reading projects.json:', error.message); - return { version: 1, workspace: '', last_modified: null, categories: [], items: [], archived: [] }; - } -} - -function writeProjects(workspacePath, data) { - const filepath = getProjectsPath(workspacePath); - data.last_modified = new Date().toISOString(); - validateSurface('projects', data); - writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); -} - -function generateProjectId(type, items) { - const prefix = TYPE_PREFIX[type]; - if (!prefix) throw new Error(`Invalid type: ${type}. Valid: initiative, project, task`); - - let max = 0; - for (const item of items) { - const match = (item.id || '').match(new RegExp(`^${prefix}-(\\d+)$`)); - if (match) { - const num = parseInt(match[1], 10); - if (num > max) max = num; - } - } - return `${prefix}-${String(max + 1).padStart(3, '0')}`; -} - -function formatTimestamp() { - return new Date().toISOString(); -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_list_projects", - description: "List/filter project items. Supports filtering by type (initiative/project/task), status, priority, parent_id, and category.", - inputSchema: { - type: "object", - properties: { - type: { type: "string", enum: ["initiative", "project", "task"], description: "Filter by hierarchy level" }, - status: { type: "string", description: "Filter by status (backlog, todo, in_progress, blocked, in_review, completed, deferred, archived)" }, - priority: { type: "string", description: "Filter by priority (urgent, high, medium, low, ongoing)" }, - parent_id: { type: "string", description: "Filter by parent item ID" }, - category: { type: "string", description: "Filter by category" } - }, - required: [] - } - }, - { - name: "base_get_project", - description: "Get a single project item by ID. Returns the full item object.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Item ID (e.g., 'PRJ-001', 'INI-002', 'TSK-005')" } - }, - required: ["id"] - } - }, - { - name: "base_add_project", - description: "Add a new initiative, project, or task. Auto-generates ID by type (INI-NNN, PRJ-NNN, TSK-NNN). Sets created_at and updated_at.", - inputSchema: { - type: "object", - properties: { - type: { type: "string", enum: ["initiative", "project", "task"], description: "Hierarchy level" }, - title: { type: "string", description: "Item title" }, - parent_id: { type: "string", description: "Parent item ID (null for top-level initiatives)" }, - status: { type: "string", enum: ["backlog", "todo", "in_progress", "blocked", "in_review", "completed", "deferred"], description: "Initial status (default: todo)" }, - priority: { type: "string", enum: ["urgent", "high", "medium", "low", "ongoing"], description: "Priority level (default: medium)" }, - category: { type: "string", description: "Category from workspace categories list" }, - assignees: { type: "array", items: { type: "string" }, description: "Entity IDs to assign" }, - description: { type: "string", description: "Extended description" }, - location: { type: "string", description: "Workspace-relative path" }, - due_date: { type: "string", description: "ISO date deadline" }, - tags: { type: "array", items: { type: "string" }, description: "Free-form tags" } - }, - required: ["type", "title"] - } - }, - { - name: "base_update_project", - description: "Update an existing item's fields by ID. Shallow merge — only specified fields updated, others preserved.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Item ID to update" }, - data: { type: "object", description: "Fields to update (shallow merge)" } - }, - required: ["id", "data"] - } - }, - { - name: "base_archive_project", - description: "Archive an item by ID — moves from items[] to archived[] with outcome and timestamp.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Item ID to archive" }, - outcome: { type: "string", description: "What happened (shipped, killed, absorbed, etc.)" } - }, - required: ["id", "outcome"] - } - }, - { - name: "base_search_projects", - description: "Search project items by keyword. Case-insensitive substring match across title, description, notes, and tags.", - inputSchema: { - type: "object", - properties: { - query: { type: "string", description: "Search query (case-insensitive)" } - }, - required: ["query"] - } - } -]; - -// ============================================================ -// TOOL HANDLERS -// ============================================================ - -function handleListProjects(args, workspacePath) { - const data = readProjects(workspacePath); - let items = data.items; - - if (args.type) items = items.filter(i => i.type === args.type); - if (args.status) items = items.filter(i => i.status === args.status); - if (args.priority) items = items.filter(i => i.priority === args.priority); - if (args.parent_id) items = items.filter(i => i.parent_id === args.parent_id); - if (args.category) items = items.filter(i => i.category === args.category); - - return { items, count: items.length, total: data.items.length }; -} - -function handleGetProject(args, workspacePath) { - const { id } = args; - if (!id) throw new Error('Missing required parameter: id'); - - const data = readProjects(workspacePath); - const item = data.items.find(i => i.id === id); - if (!item) { - throw new Error(`Item "${id}" not found. Available IDs: ${data.items.map(i => i.id).slice(0, 20).join(', ')}${data.items.length > 20 ? '...' : ''}`); - } - return item; -} - -function handleAddProject(args, workspacePath) { - const { type, title } = args; - if (!type) throw new Error('Missing required parameter: type'); - if (!title) throw new Error('Missing required parameter: title'); - - const data = readProjects(workspacePath); - const now = formatTimestamp(); - const id = generateProjectId(type, data.items); - - const newItem = { - id, - title, - type, - parent_id: args.parent_id || null, - status: args.status || 'todo', - priority: args.priority || 'medium', - category: args.category || null, - assignees: args.assignees || [], - start_date: null, - due_date: args.due_date || null, - created_at: now, - updated_at: now, - location: args.location || null, - blocked_by: null, - next: null, - notes: [], - tags: args.tags || [], - paul: null, - relations: [], - description: args.description || null - }; - - data.items.push(newItem); - writeProjects(workspacePath, data); - - debugLog(`Added ${type} ${id}: ${title}`); - return newItem; -} - -function handleUpdateProject(args, workspacePath) { - const { id, data: updateData } = args; - if (!id) throw new Error('Missing required parameter: id'); - if (!updateData) throw new Error('Missing required parameter: data'); - - const data = readProjects(workspacePath); - const index = data.items.findIndex(i => i.id === id); - if (index === -1) { - throw new Error(`Item "${id}" not found`); - } - - data.items[index] = { - ...data.items[index], - ...updateData, - id, // Prevent ID overwrite - updated_at: formatTimestamp() - }; - - writeProjects(workspacePath, data); - - debugLog(`Updated ${id}`); - return data.items[index]; -} - -function handleArchiveProject(args, workspacePath) { - const { id, outcome } = args; - if (!id) throw new Error('Missing required parameter: id'); - if (!outcome) throw new Error('Missing required parameter: outcome'); - - const data = readProjects(workspacePath); - const index = data.items.findIndex(i => i.id === id); - if (index === -1) { - throw new Error(`Item "${id}" not found`); - } - - const [item] = data.items.splice(index, 1); - const now = formatTimestamp(); - - if (!data.archived) data.archived = []; - data.archived.push({ - id: item.id, - title: item.title, - outcome, - date: now.split('T')[0], - archived_at: now - }); - - writeProjects(workspacePath, data); - - debugLog(`Archived ${id}: ${outcome}`); - return { id, title: item.title, outcome, archived_at: now }; -} - -function handleSearchProjects(args, workspacePath) { - const { query } = args; - if (!query) throw new Error('Missing required parameter: query'); - - const data = readProjects(workspacePath); - const queryLower = query.toLowerCase(); - const results = []; - - for (const item of data.items) { - const searchFields = [ - item.title, - item.description, - ...(item.tags || []), - ...(item.notes || []).map(n => n.text) - ].filter(Boolean).join(' ').toLowerCase(); - - if (searchFields.includes(queryLower)) { - results.push({ - id: item.id, - title: item.title, - type: item.type, - status: item.status, - priority: item.priority - }); - } - } - - return { results, count: results.length, query }; -} - -// ============================================================ -// HANDLER DISPATCH -// ============================================================ - -export function handleTool(name, args, workspacePath) { - switch (name) { - case 'base_list_projects': - return handleListProjects(args, workspacePath); - case 'base_get_project': - return handleGetProject(args, workspacePath); - case 'base_add_project': - return handleAddProject(args, workspacePath); - case 'base_update_project': - return handleUpdateProject(args, workspacePath); - case 'base_archive_project': - return handleArchiveProject(args, workspacePath); - case 'base_search_projects': - return handleSearchProjects(args, workspacePath); - default: - return null; - } -} diff --git a/src/packages/base-mcp/tools/psmm.js b/src/packages/base-mcp/tools/psmm.js deleted file mode 100644 index 1fbc040..0000000 --- a/src/packages/base-mcp/tools/psmm.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * BASE PSMM — Per-Session Meta Memory tools - * Tracks significant meta moments across sessions. - * The injection hook (psmm-injector.py) re-injects entries into context every prompt. - * CARL connects only for graduation: PSMM entries can be staged as CARL rule proposals. - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; - -const VALID_TYPES = ['DECISION', 'CORRECTION', 'SHIFT', 'INSIGHT', 'COMMITMENT']; - -function debugLog(...args) { - console.error('[BASE:psmm]', new Date().toISOString(), ...args); -} - -function getPsmmPath(workspacePath) { - return join(workspacePath, '.base', 'data', 'psmm.json'); -} - -function readPsmm(workspacePath) { - const filepath = getPsmmPath(workspacePath); - if (!existsSync(filepath)) { - return { sessions: {} }; - } - try { - return JSON.parse(readFileSync(filepath, 'utf-8')); - } catch (error) { - debugLog('Error reading psmm.json:', error.message); - return { sessions: {} }; - } -} - -function writePsmm(workspacePath, data) { - const filepath = getPsmmPath(workspacePath); - writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); -} - -function formatTimestamp() { - const now = new Date(); - const pad = (n) => String(n).padStart(2, '0'); - return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_psmm_log", - description: "Log a per-session meta memory entry. Types: DECISION, CORRECTION, SHIFT, INSIGHT, COMMITMENT. Auto-creates session if new.", - inputSchema: { - type: "object", - properties: { - session_id: { type: "string", description: "Session UUID" }, - type: { type: "string", enum: VALID_TYPES, description: "Entry type" }, - text: { type: "string", description: "Description of the meta moment" } - }, - required: ["session_id", "type", "text"] - } - }, - { - name: "base_psmm_get", - description: "Get all PSMM entries for a specific session by UUID.", - inputSchema: { - type: "object", - properties: { - session_id: { type: "string", description: "Session UUID" } - }, - required: ["session_id"] - } - }, - { - name: "base_psmm_list", - description: "List all PSMM sessions with entry counts and created timestamps.", - inputSchema: { type: "object", properties: {} } - }, - { - name: "base_psmm_clean", - description: "Remove a stale session's entries from PSMM.", - inputSchema: { - type: "object", - properties: { - session_id: { type: "string", description: "Session UUID to remove" } - }, - required: ["session_id"] - } - } -]; - -// ============================================================ -// TOOL HANDLERS -// ============================================================ - -export async function handleTool(name, args, workspacePath) { - switch (name) { - case "base_psmm_log": return psmmLog(args, workspacePath); - case "base_psmm_get": return psmmGet(args, workspacePath); - case "base_psmm_list": return psmmList(workspacePath); - case "base_psmm_clean": return psmmClean(args, workspacePath); - default: return null; - } -} - -async function psmmLog(args, workspacePath) { - const { session_id, type, text } = args; - - if (!VALID_TYPES.includes(type)) { - return { success: false, error: `Invalid type: ${type}. Valid: ${VALID_TYPES.join(', ')}` }; - } - - debugLog('Logging PSMM entry:', session_id, type); - - const data = readPsmm(workspacePath); - - if (!data.sessions[session_id]) { - data.sessions[session_id] = { - created: formatTimestamp(), - entries: [] - }; - } - - const entry = { - timestamp: formatTimestamp(), - type, - text - }; - - data.sessions[session_id].entries.push(entry); - writePsmm(workspacePath, data); - - return { - success: true, - session_id, - entry_count: data.sessions[session_id].entries.length, - message: `Logged ${type} entry to session ${session_id.slice(0, 8)}...` - }; -} - -async function psmmGet(args, workspacePath) { - const { session_id } = args; - debugLog('Getting PSMM for session:', session_id); - - const data = readPsmm(workspacePath); - const session = data.sessions[session_id]; - - if (!session) { - return { entries: [], exists: false }; - } - - return { - exists: true, - session_id, - created: session.created, - entry_count: session.entries.length, - entries: session.entries - }; -} - -async function psmmList(workspacePath) { - debugLog('Listing PSMM sessions'); - - const data = readPsmm(workspacePath); - const sessions = []; - - for (const [id, session] of Object.entries(data.sessions)) { - sessions.push({ - session_id: id, - created: session.created, - entry_count: session.entries.length, - types: [...new Set(session.entries.map(e => e.type))] - }); - } - - sessions.sort((a, b) => b.created.localeCompare(a.created)); - - return { - success: true, - session_count: sessions.length, - total_entries: sessions.reduce((sum, s) => sum + s.entry_count, 0), - sessions - }; -} - -async function psmmClean(args, workspacePath) { - const { session_id } = args; - debugLog('Cleaning PSMM session:', session_id); - - const data = readPsmm(workspacePath); - - if (!data.sessions[session_id]) { - return { success: false, error: `Session not found: ${session_id}` }; - } - - const entryCount = data.sessions[session_id].entries.length; - delete data.sessions[session_id]; - writePsmm(workspacePath, data); - - return { - success: true, - session_id, - entries_removed: entryCount, - message: `Cleaned session ${session_id.slice(0, 8)}... (${entryCount} entries removed)` - }; -} diff --git a/src/packages/base-mcp/tools/satellite.js b/src/packages/base-mcp/tools/satellite.js deleted file mode 100644 index 293c590..0000000 --- a/src/packages/base-mcp/tools/satellite.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * BASE Satellite Sync — Real-time PAUL project state sync - * Reads paul.json from a satellite, syncs to workspace.json + projects.json - * Called by PAUL at end of each loop phase (plan, apply, unify, handoff) - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join, relative } from 'path'; -import { validateSurface } from './validate.js'; - -function debugLog(...args) { - console.error('[BASE:satellite]', new Date().toISOString(), ...args); -} - -// ============================================================ -// HELPERS -// ============================================================ - -function readJson(filepath) { - if (!existsSync(filepath)) return null; - try { - return JSON.parse(readFileSync(filepath, 'utf-8')); - } catch (e) { - return null; - } -} - -function writeJson(filepath, data) { - writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); -} - -function formatTimestamp() { - return new Date().toISOString(); -} - -function buildPaulField(paulData, satelliteName, satellitePath) { - const phase = paulData.phase || {}; - const loop = paulData.loop || {}; - const handoff = paulData.handoff || {}; - const milestone = paulData.milestone || {}; - const timestamps = paulData.timestamps || {}; - - const completedPhases = phase.status === 'complete' - ? phase.number - : Math.max(0, (phase.number || 1) - 1); - - return { - is_paul_project: true, - satellite_name: satelliteName, - location: satellitePath + '/', - milestone: milestone.name || null, - phase: phase.name || null, - phase_name: phase.name || null, - loop_position: loop.position || 'IDLE', - last_update: timestamps.updated_at || formatTimestamp(), - handoff: handoff.present || false, - handoff_path: handoff.path || null, - completed_phases: completedPhases, - total_phases: phase.total || null, - last_plan_completed_at: paulData.last_plan_completed_at || null, - }; -} - -function findProjectByPath(items, satellitePath) { - const pathVariants = [ - satellitePath, - satellitePath + '/', - satellitePath.replace(/\/$/, ''), - ]; - return items.find(item => { - const loc = (item.location || '').replace(/\/$/, ''); - return pathVariants.some(v => v.replace(/\/$/, '') === loc); - }); -} - -function findProjectBySatelliteName(items, name) { - return items.find(item => - item.paul && item.paul.satellite_name === name - ); -} - -// ============================================================ -// SYNC LOGIC -// ============================================================ - -function syncSatellite(paulJsonPath, workspacePath) { - const paulData = readJson(paulJsonPath); - if (!paulData) throw new Error(`Cannot read paul.json at ${paulJsonPath}`); - - const name = paulData.name; - if (!name) throw new Error('paul.json has no name field'); - - // Derive paths - const projectDir = join(paulJsonPath, '..', '..'); - const satellitePath = relative(workspacePath, projectDir); - const phase = paulData.phase || {}; - const loop = paulData.loop || {}; - const handoff = paulData.handoff || {}; - const timestamps = paulData.timestamps || {}; - - const result = { satellite: name, workspace_synced: false, project_synced: false, project_created: false }; - - // --- Sync workspace.json --- - const manifestPath = join(workspacePath, '.base', 'workspace.json'); - const manifest = readJson(manifestPath); - if (manifest) { - if (!manifest.satellites) manifest.satellites = {}; - const sat = manifest.satellites[name]; - - if (sat) { - // Update existing satellite - sat.last_activity = timestamps.updated_at || formatTimestamp(); - sat.phase_name = phase.name; - sat.phase_number = phase.number; - sat.phase_status = phase.status; - sat.loop_position = loop.position; - sat.handoff = handoff.present || false; - sat.last_plan_completed_at = paulData.last_plan_completed_at; - result.workspace_synced = true; - } else { - // New satellite — register - manifest.satellites[name] = { - path: satellitePath, - engine: 'paul', - state: satellitePath + '/.paul/STATE.md', - registered: new Date().toISOString().split('T')[0], - groom_check: true, - last_activity: timestamps.updated_at || formatTimestamp(), - phase_name: phase.name, - phase_number: phase.number, - phase_status: phase.status, - loop_position: loop.position, - handoff: handoff.present || false, - last_plan_completed_at: paulData.last_plan_completed_at, - }; - result.workspace_synced = true; - } - - writeJson(manifestPath, manifest); - } - - // --- Sync projects.json --- - const projectsPath = join(workspacePath, '.base', 'data', 'projects.json'); - const projectsData = readJson(projectsPath); - if (projectsData) { - let project = findProjectBySatelliteName(projectsData.items, name) - || findProjectByPath(projectsData.items, satellitePath); - - const paulField = buildPaulField(paulData, name, satellitePath); - - if (project) { - // Update existing — merge paul field, preserve user-set fields - if (!project.paul) project.paul = {}; - Object.assign(project.paul, paulField); - project.updated_at = formatTimestamp(); - result.project_synced = true; - } else { - // Auto-create project entry - const maxNum = projectsData.items - .filter(i => (i.id || '').startsWith('PRJ-')) - .reduce((max, i) => { - const n = parseInt((i.id || '').replace('PRJ-', ''), 10); - return n > max ? n : max; - }, 0); - - const newId = `PRJ-${String(maxNum + 1).padStart(3, '0')}`; - const title = paulData.project?.title || name; - const now = formatTimestamp(); - - projectsData.items.push({ - id: newId, - title, - type: 'project', - parent_id: null, - status: 'in_progress', - priority: 'medium', - category: 'internal', - assignees: [], - start_date: null, - due_date: null, - created_at: now, - updated_at: now, - location: satellitePath + '/', - blocked_by: null, - next: null, - notes: [], - tags: [], - paul: paulField, - relations: [], - description: null, - }); - result.project_created = true; - result.project_id = newId; - } - - projectsData.last_modified = formatTimestamp(); - validateSurface('projects', projectsData); - writeJson(projectsPath, projectsData); - } - - debugLog(`Synced satellite: ${name} (ws:${result.workspace_synced}, prj:${result.project_synced}, new:${result.project_created})`); - return result; -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_sync_satellite", - description: "Sync a PAUL satellite's state to workspace.json and projects.json. Reads paul.json, updates satellite entry and matching project. Creates project entry if none exists. Call after plan/apply/unify/handoff.", - inputSchema: { - type: "object", - properties: { - path: { type: "string", description: "Workspace-relative path to the PAUL project (e.g., 'apps/my-app')" }, - }, - required: ["path"] - } - } -]; - -// ============================================================ -// HANDLER DISPATCH -// ============================================================ - -export function handleTool(name, args, workspacePath) { - switch (name) { - case 'base_sync_satellite': { - const { path: projectPath } = args; - if (!projectPath) throw new Error('Missing required parameter: path'); - - const paulJsonPath = join(workspacePath, projectPath, '.paul', 'paul.json'); - if (!existsSync(paulJsonPath)) { - throw new Error(`No paul.json found at ${projectPath}/.paul/paul.json`); - } - - return syncSatellite(paulJsonPath, workspacePath); - } - default: - return null; - } -} diff --git a/src/packages/base-mcp/tools/state.js b/src/packages/base-mcp/tools/state.js deleted file mode 100644 index 04fc7cd..0000000 --- a/src/packages/base-mcp/tools/state.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * BASE State — Read/update tools for state.json - * Workspace health, drift tracking, groom scheduling - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; -import { validateSurface } from './validate.js'; - -function debugLog(...args) { - console.error('[BASE:state]', new Date().toISOString(), ...args); -} - -// ============================================================ -// HELPERS -// ============================================================ - -function getStatePath(workspacePath) { - return join(workspacePath, '.base', 'data', 'state.json'); -} - -function readState(workspacePath) { - const filepath = getStatePath(workspacePath); - if (!existsSync(filepath)) { - return { version: 1, workspace: '', last_modified: null, groom: {}, drift: { score: 0, indicators: {} }, areas: {}, satellites: {} }; - } - try { - return JSON.parse(readFileSync(filepath, 'utf-8')); - } catch (error) { - debugLog('Error reading state.json:', error.message); - return { version: 1, workspace: '', last_modified: null, groom: {}, drift: { score: 0, indicators: {} }, areas: {}, satellites: {} }; - } -} - -function writeState(workspacePath, data) { - const filepath = getStatePath(workspacePath); - data.last_modified = new Date().toISOString(); - validateSurface('state', data); - writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); -} - -function addDays(dateStr, days) { - const d = new Date(dateStr); - d.setDate(d.getDate() + days); - return d.toISOString().split('T')[0]; -} - -function todayStr() { - return new Date().toISOString().split('T')[0]; -} - -// ============================================================ -// TOOL DEFINITIONS -// ============================================================ - -export const TOOLS = [ - { - name: "base_get_state", - description: "Read full workspace state (groom, drift, areas, satellites, carl_hygiene). Returns entire state.json.", - inputSchema: { - type: "object", - properties: {}, - required: [] - } - }, - { - name: "base_update_drift", - description: "Update drift indicators and recalculate composite score. Pass indicator key-value pairs to merge.", - inputSchema: { - type: "object", - properties: { - indicators: { - type: "object", - description: "Drift indicator updates (e.g., { active_age_days: 2, backlog_past_review: 3 })" - } - }, - required: ["indicators"] - } - }, - { - name: "base_record_groom", - description: "Record a groom event. Sets last_groom to today and advances next_groom_due based on cadence.", - inputSchema: { - type: "object", - properties: {}, - required: [] - } - }, - { - name: "base_update_area", - description: "Update a specific workspace area's fields (status, last_touched, groom_due, etc.).", - inputSchema: { - type: "object", - properties: { - area: { type: "string", description: "Area slug (key in state.json areas object)" }, - data: { type: "object", description: "Fields to merge into the area object" } - }, - required: ["area", "data"] - } - } -]; - -// ============================================================ -// TOOL HANDLERS -// ============================================================ - -function handleGetState(workspacePath) { - debugLog('Reading state'); - return readState(workspacePath); -} - -function handleUpdateDrift(args, workspacePath) { - const { indicators } = args; - if (!indicators) throw new Error('Missing required parameter: indicators'); - - debugLog('Updating drift indicators'); - const data = readState(workspacePath); - - if (!data.drift) data.drift = { score: 0, indicators: {} }; - if (!data.drift.indicators) data.drift.indicators = {}; - - // Merge indicators - data.drift.indicators = { ...data.drift.indicators, ...indicators }; - - // Recalculate score as sum of all indicator values - data.drift.score = Object.values(data.drift.indicators) - .reduce((sum, val) => sum + (typeof val === 'number' ? val : 0), 0); - - writeState(workspacePath, data); - - return { - score: data.drift.score, - indicators: data.drift.indicators - }; -} - -function handleRecordGroom(workspacePath) { - debugLog('Recording groom event'); - const data = readState(workspacePath); - - if (!data.groom) data.groom = { cadence: 'weekly', day: 'friday' }; - - const today = todayStr(); - data.groom.last_groom = today; - - // Calculate next due based on cadence - const cadenceDays = { - daily: 1, - weekly: 7, - 'bi-weekly': 14, - monthly: 30 - }; - const days = cadenceDays[data.groom.cadence] || 7; - data.groom.next_groom_due = addDays(today, days); - - writeState(workspacePath, data); - - return { - last_groom: data.groom.last_groom, - next_groom_due: data.groom.next_groom_due, - cadence: data.groom.cadence - }; -} - -function handleUpdateArea(args, workspacePath) { - const { area, data: updateData } = args; - if (!area) throw new Error('Missing required parameter: area'); - if (!updateData) throw new Error('Missing required parameter: data'); - - debugLog('Updating area:', area); - const data = readState(workspacePath); - - if (!data.areas) data.areas = {}; - if (!data.areas[area]) { - throw new Error(`Area "${area}" not found. Available: ${Object.keys(data.areas).join(', ') || 'none'}`); - } - - data.areas[area] = { ...data.areas[area], ...updateData }; - writeState(workspacePath, data); - - return data.areas[area]; -} - -// ============================================================ -// HANDLER DISPATCH -// ============================================================ - -export function handleTool(name, args, workspacePath) { - switch (name) { - case 'base_get_state': - return handleGetState(workspacePath); - case 'base_update_drift': - return handleUpdateDrift(args, workspacePath); - case 'base_record_groom': - return handleRecordGroom(workspacePath); - case 'base_update_area': - return handleUpdateArea(args, workspacePath); - default: - return null; - } -} diff --git a/src/packages/base-mcp/tools/validate.js b/src/packages/base-mcp/tools/validate.js deleted file mode 100644 index 603e003..0000000 --- a/src/packages/base-mcp/tools/validate.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Lightweight schema validation for BASE data surfaces. - * No external dependencies — enforces required fields and basic structure - * based on schemas/*.schema.json definitions. - * - * Validates on write to catch data corruption early. - * Logs warnings rather than throwing — defensive, won't block operations. - */ - -function debugLog(...args) { - console.error('[BASE:validate]', new Date().toISOString(), ...args); -} - -// ============================================================ -// SCHEMA DEFINITIONS (derived from schemas/*.schema.json) -// ============================================================ - -const SCHEMAS = { - projects: { - requiredRoot: ['items'], - itemFields: ['id', 'title', 'type', 'status', 'priority', 'created_at', 'updated_at'], - validTypes: ['initiative', 'project', 'task'], - validStatuses: ['backlog', 'todo', 'in_progress', 'blocked', 'in_review', 'completed', 'deferred', 'archived'], - validPriorities: ['urgent', 'high', 'medium', 'low', 'ongoing'], - idPattern: /^(INI|PRJ|TSK)-\d{3,}$/ - }, - entities: { - requiredRoot: ['entities'], - itemFields: ['id', 'name', 'type', 'created_at', 'updated_at'], - validTypes: ['person', 'organization'], - idPattern: /^ENT-\d{3,}$/ - }, - state: { - requiredRoot: ['groom', 'drift', 'areas'], - requiredGroom: ['cadence'], - validCadences: ['daily', 'weekly', 'bi-weekly', 'monthly'], - validAreaStatuses: ['current', 'stale', 'critical'] - } -}; - -// ============================================================ -// VALIDATORS -// ============================================================ - -/** - * Validate a data surface before writing. - * Returns { valid: boolean, warnings: string[] } - */ -export function validateSurface(surfaceName, data) { - const schema = SCHEMAS[surfaceName]; - if (!schema) { - return { valid: true, warnings: [] }; - } - - const warnings = []; - - // Check required root fields - if (schema.requiredRoot) { - for (const field of schema.requiredRoot) { - if (data[field] === undefined) { - warnings.push(`Missing required root field: ${field}`); - } - } - } - - // Validate items array (projects, entities) - if (schema.itemFields && Array.isArray(data.items || data.entities)) { - const items = data.items || data.entities; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - for (const field of schema.itemFields) { - if (item[field] === undefined) { - warnings.push(`Item ${item.id || `[${i}]`}: missing required field '${field}'`); - } - } - - // Validate ID pattern - if (schema.idPattern && item.id && !schema.idPattern.test(item.id)) { - warnings.push(`Item ${item.id}: ID does not match pattern ${schema.idPattern}`); - } - - // Validate enum fields - if (schema.validTypes && item.type && !schema.validTypes.includes(item.type)) { - warnings.push(`Item ${item.id || `[${i}]`}: invalid type '${item.type}'`); - } - if (schema.validStatuses && item.status && !schema.validStatuses.includes(item.status)) { - warnings.push(`Item ${item.id || `[${i}]`}: invalid status '${item.status}'`); - } - if (schema.validPriorities && item.priority && !schema.validPriorities.includes(item.priority)) { - warnings.push(`Item ${item.id || `[${i}]`}: invalid priority '${item.priority}'`); - } - } - } - - // State-specific validation - if (surfaceName === 'state') { - if (data.groom && schema.requiredGroom) { - for (const field of schema.requiredGroom) { - if (data.groom[field] === undefined) { - warnings.push(`groom: missing required field '${field}'`); - } - } - if (data.groom.cadence && !schema.validCadences.includes(data.groom.cadence)) { - warnings.push(`groom: invalid cadence '${data.groom.cadence}'`); - } - } - if (data.areas) { - for (const [areaName, area] of Object.entries(data.areas)) { - if (area.status && !schema.validAreaStatuses.includes(area.status)) { - warnings.push(`area '${areaName}': invalid status '${area.status}'`); - } - } - } - } - - if (warnings.length > 0) { - debugLog(`Validation warnings for ${surfaceName}:`, warnings); - } - - return { valid: warnings.length === 0, warnings }; -} diff --git a/src/skill/base.md b/src/skill/base.md deleted file mode 100644 index 5cd450d..0000000 --- a/src/skill/base.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: base -type: suite -version: 0.1.0 -category: workspace-orchestration -description: "Builder's Automated State Engine — workspace lifecycle management for Claude Code. Scaffold, audit, groom, and maintain AI builder workspaces. Manage data surfaces for structured context injection. Use when user mentions workspace setup, cleanup, organization, maintenance, grooming, auditing workspace health, surfaces, or BASE." -allowed-tools: [Read, Write, Glob, Grep, Edit, Bash, Agent, AskUserQuestion] ---- - - - -## What -BASE (Builder's Automated State Engine) manages the lifecycle of a Claude Code workspace. It scaffolds new workspaces, audits existing ones, runs structured grooming cycles, and maintains workspace health through automated drift detection. - -## When to Use -- User says "base", "workspace", "cleanup", "organize", "audit my workspace", "groom", "surface", "create a surface" -- User wants to set up a new workspace from scratch -- User wants to optimize or clean up an existing workspace -- User asks about workspace health, staleness, or drift -- Session start hook detects overdue grooming -- User wants to review workspace evolution history - -## Not For -- Project-level build orchestration (that's PAUL) -- Session-level rule management (that's CARL) -- Code quality auditing (that's AEGIS) -- Skill/tool creation (that's Skillsmith) - - - - - -## Role -Workspace operations engineer. Knows the territory, tracks what's drifting, enforces maintenance cadence. Tactical, not theoretical. - -## Style -- Direct, structured, checklist-driven -- Presents health dashboards and drift scores -- Asks focused questions during grooming (voice-friendly) -- Never skips areas — systematic coverage -- Recommends, doesn't dictate - -## Expertise -- Workspace architecture and file organization -- Context document lifecycle (projects.json, state.json, entities.json) -- Tool and configuration management -- Drift detection and prevention patterns -- Claude Code ecosystem (PAUL, CARL, AEGIS, Skillsmith integration) - - - - - -| Command | Description | Routes To | -|---------|------------|-----------| -| `/base:pulse` | Daily activation — workspace health briefing | `@~/.claude/base-framework/tasks/pulse.md` | -| `/base:groom` | Weekly maintenance cycle | `@~/.claude/base-framework/tasks/groom.md` | -| `/base:audit` | Deep workspace optimization | `@~/.claude/base-framework/tasks/audit.md` | -| `/base:scaffold` | Set up BASE in a new workspace | `@~/.claude/base-framework/tasks/scaffold.md` | -| `/base:status` | Quick health check (one-liner) | `@~/.claude/base-framework/tasks/status.md` | -| `/base:history` | Workspace evolution timeline | `@~/.claude/base-framework/tasks/history.md` | -| `/base:audit-claude-md` | Audit CLAUDE.md, generate recommended version | `@~/.claude/base-framework/tasks/audit-claude-md.md` | -| `/base:carl-hygiene` | CARL domain maintenance and rule review | `@~/.claude/base-framework/tasks/carl-hygiene.md` | -| `/base:surface create` | Create a new data surface (guided) | `@~/.claude/base-framework/tasks/surface-create.md` | -| `/base:surface convert` | Convert markdown file to data surface | `@~/.claude/base-framework/tasks/surface-convert.md` | -| `/base:surface list` | Show all registered surfaces | `@~/.claude/base-framework/tasks/surface-list.md` | - - - - - -## Always Load -- `@~/.claude/base-framework/context/base-principles.md` — Core workspace management principles -- `@~/.claude/base-framework/frameworks/audit-strategies.md` — Reusable audit strategy definitions - -## Load on Command -- `@~/.claude/base-framework/tasks/pulse.md` — on `/base:pulse` -- `@~/.claude/base-framework/tasks/groom.md` — on `/base:groom` -- `@~/.claude/base-framework/tasks/audit.md` — on `/base:audit` -- `@~/.claude/base-framework/tasks/scaffold.md` — on `/base:scaffold` -- `@~/.claude/base-framework/tasks/status.md` — on `/base:status` -- `@~/.claude/base-framework/tasks/history.md` — on `/base:history` -- `@~/.claude/base-framework/tasks/carl-hygiene.md` — on `/base:carl-hygiene` -- `@~/.claude/base-framework/tasks/surface-create.md` — on `/base:surface create` -- `@~/.claude/base-framework/tasks/surface-convert.md` — on `/base:surface convert` -- `@~/.claude/base-framework/tasks/surface-list.md` — on `/base:surface list` - -## Load on Demand -- `@~/.claude/base-framework/templates/workspace-json.md` — When generating workspace.json -- `@~/.claude/base-framework/frameworks/satellite-registration.md` — When handling PAUL project registration - - - - - -BASE loaded. Builder's Automated State Engine. - -Available commands: -- `/base:pulse` — What's the state of my workspace? -- `/base:groom` — Run weekly maintenance -- `/base:audit` — Deep optimization session -- `/base:scaffold` — Set up BASE in a new workspace -- `/base:status` — Quick health check -- `/base:history` — Workspace evolution timeline -- `/base:surface create` — Create a new data surface -- `/base:surface list` — Show registered surfaces - -What do you need? - - From 164ab2a36786baf2e703182aecf67c50a209368b Mon Sep 17 00:00:00 2001 From: Madison Steiner <8176115+mh0pe@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:14:00 -0700 Subject: [PATCH 4/6] fix(plugin): bootstrap native-plugin MCP deps via SessionStart + symlink Add committed hooks/install-mcp-deps.py (SessionStart hook) that installs @modelcontextprotocol/sdk deps into CLAUDE_PLUGIN_DATA and symlinks PLUGIN_ROOT/mcp/node_modules -> PLUGIN_DATA/node_modules so Node ESM can resolve bare specifiers from the importing file's directory. The MCP server lives at mcp/index.js (not mcp/base-mcp/index.js as in the install-skills-dir branch). The symlink is placed at mcp/node_modules adjacent to mcp/index.js so ESM's directory-walk resolution succeeds. NODE_PATH added to .mcp.json env (CJS fallback; Node ESM requires the symlink bridge since ESM does not honour NODE_PATH at import time). - Idempotent: skips install if sdk marker already present; re-asserts symlink - Fail-open: warns to stderr and exits 0 on any error (npm missing, env unset) - Keeps 5 UserPromptSubmit hooks + satellite-detection SessionStart hook - Adds install-mcp-deps as second SessionStart hook entry - npx-mode safety: hook is harmless/fail-open when CLAUDE_PLUGIN_DATA unset Co-Authored-By: Claude Opus 4.8 (1M context) --- .mcp.json | 3 +- hooks/hooks.json | 8 +++ hooks/install-mcp-deps.py | 112 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 hooks/install-mcp-deps.py diff --git a/.mcp.json b/.mcp.json index ef001fd..c56eafe 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,7 +4,8 @@ "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/index.js"], "env": { - "CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}" + "CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}", + "NODE_PATH": "${CLAUDE_PLUGIN_DATA}/node_modules" } } } diff --git a/hooks/hooks.json b/hooks/hooks.json index 675ed20..bd846c0 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -50,6 +50,14 @@ "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/satellite-detection.py\"" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3.11 \"${CLAUDE_PLUGIN_ROOT}/hooks/install-mcp-deps.py\"" + } + ] } ] } diff --git a/hooks/install-mcp-deps.py b/hooks/install-mcp-deps.py new file mode 100644 index 0000000..1c0cfed --- /dev/null +++ b/hooks/install-mcp-deps.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +SessionStart hook: install base-mcp npm dependencies into CLAUDE_PLUGIN_DATA. + +Idempotent: exits 0 immediately if @modelcontextprotocol/sdk is already present. +Fail-open: warns to stderr and exits 0 on any error so the session always starts. + +Strategy: + 1. npm install --omit=dev --prefix "$CLAUDE_PLUGIN_DATA" + -> places node_modules at $CLAUDE_PLUGIN_DATA/node_modules/ + 2. symlink $CLAUDE_PLUGIN_ROOT/mcp/node_modules + -> $CLAUDE_PLUGIN_DATA/node_modules + Node ESM resolves bare specifiers by walking up from the importing file, so + node_modules must live adjacent to index.js. NODE_PATH is honored only by + the CommonJS loader (node:internal/modules/cjs), not the ESM loader; this + symlink bridges the two locations so ESM resolution succeeds. + The MCP server lives at ${CLAUDE_PLUGIN_ROOT}/mcp/index.js, so the symlink + target is ${CLAUDE_PLUGIN_ROOT}/mcp/node_modules. +""" +import os +import sys +import shutil +import subprocess + + +def warn(msg): + print(f"[install-mcp-deps] WARNING: {msg}", file=sys.stderr) + + +def main(): + plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "").strip() + plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA", "").strip() + + if not plugin_data: + warn("CLAUDE_PLUGIN_DATA is not set; skipping MCP deps install.") + sys.exit(0) + + if not plugin_root: + warn("CLAUDE_PLUGIN_ROOT is not set; skipping MCP deps install.") + sys.exit(0) + + sdk_marker = os.path.join(plugin_data, "node_modules", "@modelcontextprotocol", "sdk") + # MCP server lives at ${CLAUDE_PLUGIN_ROOT}/mcp/index.js — symlink goes next to it + mcp_dir = os.path.join(plugin_root, "mcp") + mcp_nm = os.path.join(mcp_dir, "node_modules") + + # Idempotent: if sdk already installed, just (re)assert symlink and exit + if os.path.isdir(sdk_marker): + # Re-assert symlink so it survives if the plugin dir was refreshed + _assert_symlink(mcp_nm, plugin_data) + sys.exit(0) + + # Source package.json is at ${CLAUDE_PLUGIN_ROOT}/mcp/package.json + src_pkg = os.path.join(mcp_dir, "package.json") + if not os.path.isfile(src_pkg): + warn(f"package.json not found at {src_pkg}; skipping.") + sys.exit(0) + + try: + os.makedirs(plugin_data, exist_ok=True) + shutil.copy2(src_pkg, os.path.join(plugin_data, "package.json")) + + lockfile = os.path.join(mcp_dir, "package-lock.json") + if os.path.isfile(lockfile): + shutil.copy2(lockfile, os.path.join(plugin_data, "package-lock.json")) + + npm = shutil.which("npm") + if not npm: + warn("npm not found in PATH; skipping MCP deps install.") + sys.exit(0) + + result = subprocess.run( + [npm, "install", "--omit=dev", "--prefix", plugin_data], + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode != 0: + warn(f"npm install failed (exit {result.returncode}): {result.stderr.strip()}") + sys.exit(0) + + _assert_symlink(mcp_nm, plugin_data) + print(f"[install-mcp-deps] MCP deps installed to {plugin_data}", file=sys.stderr) + + except Exception as exc: + warn(f"Unexpected error during MCP deps install: {exc}") + sys.exit(0) + + +def _assert_symlink(link_path, plugin_data): + """Create or update the node_modules symlink inside the MCP dir.""" + target = os.path.join(plugin_data, "node_modules") + try: + # Remove stale symlink so we can set the correct target + if os.path.islink(link_path): + if os.readlink(link_path) == target: + return # already correct + os.unlink(link_path) + elif os.path.isdir(link_path): + # A real node_modules exists (e.g. from a previous local install); + # leave it alone so we don't break a working setup. + return + os.symlink(target, link_path) + except Exception as exc: + print( + f"[install-mcp-deps] WARNING: could not assert symlink {link_path} -> {target}: {exc}", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() From 48820b400a4819113437da596d80bc496e99a84e Mon Sep 17 00:00:00 2001 From: Madison Steiner <8176115+mh0pe@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:25:40 -0700 Subject: [PATCH 5/6] ci: add plugin-install workflow; fix(plugin): ignore mcp/node_modules symlink - .gitignore: add slash-less `mcp/node_modules` entry so the SessionStart symlink (created by install-mcp-deps.py) is properly ignored. The existing `node_modules/` pattern matches directories only; a trailing slash does NOT match symlinks, causing the link to appear as untracked. - .github/workflows/plugin-install.yml: validate + auth-free install smoke on every push/PR (identical shape to paul/seed/carl native workflows). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/plugin-install.yml | 26 ++++++++++++++++++++++++++ .gitignore | 4 ++++ 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/plugin-install.yml diff --git a/.github/workflows/plugin-install.yml b/.github/workflows/plugin-install.yml new file mode 100644 index 0000000..8f812c5 --- /dev/null +++ b/.github/workflows/plugin-install.yml @@ -0,0 +1,26 @@ +name: plugin-install + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + validate-and-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install Claude Code CLI + run: npm install -g @anthropic-ai/claude-code + - name: Validate plugin + marketplace manifest (strict) + run: claude plugin validate . --strict + - name: Install smoke test (claude can install the plugin) + run: | + set -euo pipefail + claude plugin marketplace add . + claude plugin install base@base + claude plugin list + claude plugin list | grep -i base diff --git a/.gitignore b/.gitignore index 724a137..8acf352 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ DIRECTORY-STRATEGY-SPEC.md # Node node_modules/ +# mcp/node_modules is created as a symlink by the SessionStart hook; +# the trailing-slash pattern above matches directories but NOT symlinks, +# so we add a slash-less entry to cover the symlink case as well. +mcp/node_modules __pycache__/ From 75ca938053b0ded0fe15d344c0aa681208f6790c Mon Sep 17 00:00:00 2001 From: Madison Steiner <8176115+mh0pe@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:41:50 -0700 Subject: [PATCH 6/6] ci: fix marketplace add path (./ not .) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/plugin-install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-install.yml b/.github/workflows/plugin-install.yml index 8f812c5..6ee0092 100644 --- a/.github/workflows/plugin-install.yml +++ b/.github/workflows/plugin-install.yml @@ -20,7 +20,7 @@ jobs: - name: Install smoke test (claude can install the plugin) run: | set -euo pipefail - claude plugin marketplace add . + claude plugin marketplace add ./ claude plugin install base@base claude plugin list claude plugin list | grep -i base