diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..327c81b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Force LF line endings for shell scripts — prevents CRLF breakage on Windows +*.sh text eol=lf +*.py text eol=lf +*.md text eol=lf +*.yaml text eol=lf +*.json text eol=lf diff --git a/CHANGELOG.md b/CHANGELOG.md index 771f32b..326aaff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ The GTM release. Two weeks live, 500+ installs, and a full architectural refacto - **Author:** Stack Walnuts → Lock-in Lab - **Category:** plugin → pcm (Personal Context Manager) - **Twitter:** @ALIVE_context -- **All 5 rules rewritten** for v3 (version 3.0.0) +- **All 6 rules rewritten** for v3 (version 3.0.0) - **All 15 skills updated** — 6 major rewrites, 9 moderate/minor - **5 hooks updated** — project.py trigger, v3 paths, backward compat - **generate-index.py** — reads v3 flat now.json, extracts task counts, includes recent sessions and unsigned stash count diff --git a/README.md b/README.md index 26aa296..ef8529f 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The squirrel is the agent runtime — rules, hooks, skills, and policies that an │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Rules │ │ Skills │ │ Hooks │ │ -│ │ 6 files │ │ 15 skills │ │ 14 hooks │ │ +│ │ 6 files │ │ 15 skills │ │ 13 hooks │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ @@ -183,7 +183,7 @@ claude plugin install alive@alivecontext Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) + Python 3. Works on macOS, Linux, Windows (WSL). -15 skills, 14 hooks, 6 rule files, templates, and a statusline. +15 skills, 13 hooks, 6 rule files, templates, and a statusline. ### Skills diff --git a/plugins/alive/hooks/hooks.json b/plugins/alive/hooks/hooks.json index 3cd321f..d5133cb 100644 --- a/plugins/alive/hooks/hooks.json +++ b/plugins/alive/hooks/hooks.json @@ -1,5 +1,5 @@ { - "description": "ALIVE Context System v2 — 14 hooks. Session hooks read/write .alive/_squirrels/. All read stdin JSON for session_id.", + "description": "ALIVE Context System v3 — 13 hooks. Session hooks read/write .alive/_squirrels/. All read stdin JSON for session_id.", "hooks": { "SessionStart": [ { diff --git a/plugins/alive/hooks/scripts/alive-common.sh b/plugins/alive/hooks/scripts/alive-common.sh index 76977d9..4eee540 100644 --- a/plugins/alive/hooks/scripts/alive-common.sh +++ b/plugins/alive/hooks/scripts/alive-common.sh @@ -11,8 +11,16 @@ fi # -- JSON runtime detection -- # python3 preferred (fast). node guaranteed (Claude Code is a Node app). +# Windows ships a python3 Store stub (AppInstallerPythonRedirector.exe) that +# passes command -v but fails to execute (exit code 49). We validate execution, +# not just existence. The py -3 launcher is the standard Windows Python path. ALIVE_JSON_RT="" -if command -v python3 &>/dev/null; then +if command -v python3 &>/dev/null && python3 -c "" &>/dev/null 2>&1; then + ALIVE_JSON_RT="python3" +elif command -v py &>/dev/null && py -3 -c "" &>/dev/null 2>&1; then + # Windows py launcher: shim python3 so all existing callsites work + python3() { py -3 "$@"; } + export -f python3 ALIVE_JSON_RT="python3" elif command -v node &>/dev/null; then ALIVE_JSON_RT="node" @@ -51,7 +59,7 @@ const d=JSON.parse(require('fs').readFileSync(0,'utf8')); # Read JSON input from stdin. Must be called BEFORE any other stdin read. # Sets: HOOK_INPUT, HOOK_SESSION_ID, HOOK_CWD, HOOK_EVENT read_hook_input() { - HOOK_INPUT=$(cat /dev/stdin 2>/dev/null || echo '{}') + HOOK_INPUT=$(cat 2>/dev/null || echo '{}') local parsed parsed=$(_json_multi "$HOOK_INPUT" "session_id cwd hook_event_name") HOOK_SESSION_ID=$(echo "$parsed" | sed -n '1p') diff --git a/plugins/alive/rules/squirrels.md b/plugins/alive/rules/squirrels.md index 404b859..524d4a5 100644 --- a/plugins/alive/rules/squirrels.md +++ b/plugins/alive/rules/squirrels.md @@ -161,7 +161,7 @@ Do not panic about context usage. Do not suggest ending a session, starting a fr Always have enough state on disk that a crash, compaction, or abrupt exit doesn't lose the session. This means: -- Stash checkpoint every 5 items or 20 minutes (shadow-write to squirrel YAML) +- Save IS the checkpoint — no automatic mid-session shadow-writes. If the session crashes before a save, the transcript JSONL is the recovery source (via `alive:session-context-rebuild`). - Action log maintained in squirrel YAML throughout the session - `recovery_state` written to squirrel YAML so the next session knows exactly where things stopped @@ -578,6 +578,6 @@ Mid-session saves reset the stash but don't end the session. The squirrel return - Stash on change only. No change = no stash shown. - Every stash add includes a remove prompt (-> drop?) - If 30+ minutes pass without stashing, scan back — decisions were probably made -- Stash checkpoint: every 5 items or 20 minutes, shadow-write to squirrel YAML (crash insurance) +- Stash checkpoint: save IS the checkpoint — no automatic mid-session shadow-writes - Resolved questions don't stay in stash — they become decisions (log) or insights (if evergreen) - At save: group by type (decisions / tasks / notes / insight candidates) diff --git a/plugins/alive/scripts/generate-graph.py b/plugins/alive/scripts/generate-graph.py index e037cc2..00457a8 100644 --- a/plugins/alive/scripts/generate-graph.py +++ b/plugins/alive/scripts/generate-graph.py @@ -31,8 +31,12 @@ def main(): json_file = os.path.join(world_root, '.alive', '_index.json') html_file = os.path.join(world_root, '.alive', 'context-graph.html') - with open(json_file, 'r', encoding='utf-8') as f: - data = json.load(f) + try: + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + except (IOError, json.JSONDecodeError) as e: + print(f"Error reading {json_file}: {e}", file=sys.stderr) + sys.exit(1) stats = data['stats'] walnuts = data['walnuts'] @@ -54,7 +58,7 @@ def main(): try: days_since = (datetime.strptime(today, '%Y-%m-%d') - datetime.strptime(updated, '%Y-%m-%d')).days - except: + except (ValueError, TypeError, KeyError): days_since = 30 if capsule_count >= 15: size = 20 @@ -129,7 +133,7 @@ def main(): import re key_path = os.path.join(world_root, '.alive', 'key.md') if os.path.exists(key_path): - with open(key_path) as f: + with open(key_path, encoding='utf-8') as f: content = f.read() m = re.search(r'^name:\s*(.+)$', content, re.MULTILINE) if m: @@ -148,7 +152,7 @@ def main(): try: key_path = os.path.join(world_root, '.alive', 'key.md') if os.path.exists(key_path): - with open(key_path) as f: + with open(key_path, encoding='utf-8') as f: content = f.read() # Parse wikilinks from links: field links_match = re.search(r'^links:\s*(.+)$', content, re.MULTILINE) diff --git a/plugins/alive/scripts/generate-index.py b/plugins/alive/scripts/generate-index.py index 4f30182..5cd524a 100644 --- a/plugins/alive/scripts/generate-index.py +++ b/plugins/alive/scripts/generate-index.py @@ -24,7 +24,6 @@ import re import json from datetime import datetime, timezone -from pathlib import Path def extract_frontmatter(filepath): diff --git a/plugins/alive/scripts/tasks.py b/plugins/alive/scripts/tasks.py index eac00b3..fa98902 100755 --- a/plugins/alive/scripts/tasks.py +++ b/plugins/alive/scripts/tasks.py @@ -7,12 +7,12 @@ """ import argparse +import getpass import json import os import re import sys from datetime import datetime, timedelta -from pathlib import Path # --------------------------------------------------------------------------- @@ -288,7 +288,7 @@ def cmd_done(args): task["status"] = "done" task["completed"] = _today() - task["completed_by"] = args.by or os.environ.get("USER", "unknown") + task["completed_by"] = args.by or getpass.getuser() completed_data["completed"].append(task) _atomic_write(completed_path, completed_data) diff --git a/plugins/alive/skills/load-context/SKILL.md b/plugins/alive/skills/load-context/SKILL.md index 312fa47..1ed2648 100644 --- a/plugins/alive/skills/load-context/SKILL.md +++ b/plugins/alive/skills/load-context/SKILL.md @@ -141,7 +141,7 @@ If `now.json` has a `bundle:` field pointing to an active bundle, offer to deep- **Deep load reads:** -1. **`bundles/{name}/context.manifest.yaml`** — full file (context, changelog, work log, session history) +1. **`{name}/context.manifest.yaml`** — full file (context, changelog, work log, session history) 2. **`tasks.py list --walnut {path} --bundle {name}`** — call the script for the detailed task view. Do NOT read `tasks.json` directly; the script is the interface. 3. **Write `active_sessions:` entry** to the bundle's `context.manifest.yaml` — claim this session so other agents know you're here. diff --git a/plugins/alive/skills/save/SKILL.md b/plugins/alive/skills/save/SKILL.md index d38ad69..fa0b5ba 100644 --- a/plugins/alive/skills/save/SKILL.md +++ b/plugins/alive/skills/save/SKILL.md @@ -22,7 +22,7 @@ Read these in parallel before presenting the stash or writing anything: - `_kernel/log.md` — first ~100 lines (recent entries — what have previous sessions covered?) - Active bundle's `context.manifest.yaml` — if `now.json` has a `next.bundle` value, read that bundle's manifest -**Do NOT read `bundles/*/tasks.md`** — task data lives in `now.json` already, or call `tasks.py list --walnut {path}` if you need specific detail. +**Do NOT read task files directly** — task data lives in `now.json` already, or call `tasks.py list --walnut {path}` if you need specific detail. **Backward compat:** If `_kernel/now.json` does not exist, check `_kernel/_generated/now.json` as a fallback. diff --git a/plugins/alive/skills/search-world/SKILL.md b/plugins/alive/skills/search-world/SKILL.md index dd6e612..7d1c627 100644 --- a/plugins/alive/skills/search-world/SKILL.md +++ b/plugins/alive/skills/search-world/SKILL.md @@ -69,7 +69,7 @@ Find searches across ALL walnuts by default. Results show which walnut each matc │ 2. nova-station / _kernel/log.md — 2026-02-23 │ Decision: go with hybrid shielding approach │ -│ 3. nova-station / bundles/research/ +│ 3. nova-station / research/ │ 2026-02-23-radiation-shielding-options.md │ │ number to load, or refine search. diff --git a/plugins/alive/skills/world/setup.md b/plugins/alive/skills/world/setup.md index 90254f2..800bc01 100644 --- a/plugins/alive/skills/world/setup.md +++ b/plugins/alive/skills/world/setup.md @@ -484,13 +484,11 @@ For each walnut in the list: ``` {{domain}}/{{slug}}/ {{domain}}/{{slug}}/_kernel/ -{{domain}}/{{slug}}/_kernel/_generated/ -{{domain}}/{{slug}}/bundles/ ``` **Create walnut files from templates:** -For each file in `templates/walnut/` (key.md, now.json, log.md, tasks.md, insights.md): +For each file in `templates/walnut/` (key.md, log.md, insights.md): Read the template. Replace variables: - `{{name}}` → walnut display name (original casing) @@ -505,16 +503,21 @@ For key.md specifically: - Set `rhythm:` to the walnut's rhythm value - If people are associated with this walnut, fill the `## Key People` section -Write each file to `{{domain}}/{{slug}}/_kernel/{{filename}}` (with now.json going to `_kernel/_generated/now.json`). +Write each file to `{{domain}}/{{slug}}/_kernel/{{filename}}`. + +Additionally, create these JSON files directly (not from templates): +- `_kernel/tasks.json` with content `{"tasks": []}` +- `_kernel/completed.json` with content `{"completed": []}` +- `_kernel/now.json` is generated by `project.py` post-save -- do not create manually Show: ``` │ ▸ {{domain}}/{{slug}}/ │ ▸ _kernel/key.md — "{{goal}}" -│ ▸ _kernel/_generated/now.json — phase: starting │ ▸ _kernel/log.md — first entry signed │ ▸ _kernel/insights.md — empty, ready -│ ▸ bundles/ — empty, ready +│ ▸ _kernel/tasks.json — empty queue +│ ▸ _kernel/completed.json — empty archive ``` #### Step 6: Create people walnuts @@ -527,8 +530,6 @@ For each person in the list: ``` 02_Life/people/{{slug}}/ 02_Life/people/{{slug}}/_kernel/ -02_Life/people/{{slug}}/_kernel/_generated/ -02_Life/people/{{slug}}/bundles/ ``` **Create walnut files from templates:** @@ -597,11 +598,11 @@ Display this summary. Fill in actual values for every placeholder. | `.alive/overrides.md` | User rule customizations (never overwritten by updates) | | `.alive/_squirrels/` | Centralized session entries | | `[walnut]/_kernel/key.md` | Walnut identity and standing context | -| `[walnut]/_kernel/_generated/now.json` | Current state synthesis (generated) | +| `[walnut]/_kernel/now.json` | Current state synthesis (generated by project.py) | | `[walnut]/_kernel/log.md` | Prepend-only event spine | -| `[walnut]/bundles/*/tasks.md` | Work queue per bundle | +| `[walnut]/_kernel/tasks.json` | Task queue (script-operated via tasks.py) | +| `[walnut]/_kernel/completed.json` | Completed/dropped task archive | | `[walnut]/_kernel/insights.md` | Evergreen domain knowledge | -| `[walnut]/bundles/` | Self-contained units of work | ## What Setup Does NOT Do diff --git a/plugins/alive/statusline/alive-statusline.sh b/plugins/alive/statusline/alive-statusline.sh index 32ed841..983a5d1 100755 --- a/plugins/alive/statusline/alive-statusline.sh +++ b/plugins/alive/statusline/alive-statusline.sh @@ -3,7 +3,7 @@ # Boot message on first render, then working statusline after first response. # Cross-platform: Mac, Linux, Windows (Git Bash). No python3 dependency. -INPUT=$(cat /dev/stdin 2>/dev/null || echo '{}') +INPUT=$(cat 2>/dev/null || echo '{}') # ── Platform detection ── ALIVE_PLATFORM="unix" diff --git a/plugins/alive/templates/walnut/key.md b/plugins/alive/templates/walnut/key.md index 02c2482..f8fe200 100644 --- a/plugins/alive/templates/walnut/key.md +++ b/plugins/alive/templates/walnut/key.md @@ -32,7 +32,7 @@ published: [] ## Context