@esoteric-logic/praxis-harness 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,12 @@ For technical research: `/discover` (structured options evaluation before decisi
66
66
  | `risk` | Add a risk register entry to the vault |
67
67
  | `kit` | Activate/deactivate an AI-Kit |
68
68
  | `review` | Manual code review via subagent |
69
+ | `simplify` | Post-implementation code simplification via subagent |
69
70
  | `debug` | Structured test-first debugging |
71
+ | `ship` | Commit, push, and PR in one command with pre-flight checks |
72
+ | `verify-app` | End-to-end verification with regression analysis |
73
+ | `session-retro` | End-of-session retrospective with learnings extraction |
74
+ | `status-update` | Manual vault status.md update |
70
75
  | `context-reset` | Reload context from vault without clearing session |
71
76
 
72
77
  ## Rules
@@ -126,13 +131,57 @@ The vault path is configured per machine during install:
126
131
 
127
132
  Requires [Obsidian CLI](https://obsidian.md) (enable in Obsidian Settings > General > Command line interface). Obsidian must be running for vault search.
128
133
 
134
+ ### What gets documented automatically
135
+
136
+ Praxis auto-documents your work in the vault with zero manual effort. Two independent layers ensure nothing is lost:
137
+
138
+ 1. **Shell hooks** capture facts (git state, timestamps) even if Claude runs out of context
139
+ 2. **Stop prompt** captures meaning (summaries, decisions, learnings) from conversation context
140
+
141
+ **At session end** (zero action needed):
142
+ - `status.md` — updated with What/So What/Now What
143
+ - `claude-progress.json` — session entry with summary, accomplishments, milestones, features
144
+ - `notes/{date}_session-note.md` — session summary, decisions, learnings, next steps
145
+ - `notes/decision-log.md` — checkpoint decisions, scope changes (appended)
146
+ - `notes/learnings.md` — [LEARN:tag] pattern entries (appended)
147
+ - `specs/` — ADRs for architectural decisions made during the session
148
+
149
+ **During workflow skills** (automatic within each skill):
150
+
151
+ | Skill | Auto-writes to vault |
152
+ |-------|---------------------|
153
+ | `/execute` | `status.md` loop position, `decision-log.md` scope events |
154
+ | `/verify` | `claude-progress.json` milestones[] |
155
+ | `/review` | `specs/review-{date}-{slug}.md` (full findings breakdown) |
156
+ | `/simplify` | `notes/{date}_simplify-findings.md` |
157
+ | `/debug` | `notes/{date}_debug-trace.md` |
158
+ | `/verify-app` | `specs/verify-app-{date}-{slug}.md` |
159
+ | `/ship` | `claude-progress.json` features[] |
160
+
161
+ **On context compaction** (automatic fallback):
162
+ - `plans/{date}-compact-checkpoint.md` — git state, active plan, loop position
163
+ - `claude-progress.json` — session entry preserved
164
+
129
165
  ## Updating
130
166
 
167
+ ### Updating the harness
168
+
131
169
  ```bash
132
170
  npx praxis-harness update
133
171
  ```
134
172
 
135
- Re-copies all files from the latest npm package version. Config file is preserved.
173
+ Re-copies all hooks, skills, rules, and kits from the latest npm package version. Config file is preserved.
174
+
175
+ ### Updating existing projects
176
+
177
+ After a harness update that adds new vault files (like `decision-log.md`), run `/scaffold-exist` in a Claude Code session to audit your vault and add any missing files. This is non-destructive — it never overwrites existing content.
178
+
179
+ ```
180
+ Step 1: npx praxis-harness update → deploys new hooks, skills, rules to ~/.claude/
181
+ Step 2: /scaffold-exist → audits vault, adds missing files
182
+ ```
183
+
184
+ New projects get everything automatically via `/scaffold-new`.
136
185
 
137
186
  ## Uninstalling
138
187
 
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook — collects structured session data and stages it for the Stop prompt.
3
+ # Always exits 0 (advisory, never blocks session end).
4
+ set -uo pipefail
5
+
6
+ CONFIG_FILE="$HOME/.claude/praxis.config.json"
7
+
8
+ if [[ ! -f "$CONFIG_FILE" ]]; then
9
+ exit 0
10
+ fi
11
+
12
+ VAULT_PATH=$(jq -r '.vault_path // empty' "$CONFIG_FILE" 2>/dev/null)
13
+ if [[ -z "$VAULT_PATH" || ! -d "$VAULT_PATH" ]]; then
14
+ exit 0
15
+ fi
16
+
17
+ DATE=$(date +%Y-%m-%d)
18
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
19
+
20
+ # Git state (fail gracefully if not in a repo)
21
+ BRANCH=$(git --no-pager rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
22
+ LAST_COMMIT=$(git --no-pager log --oneline -1 2>/dev/null || echo "no commits")
23
+ RECENT_COMMITS=$(git --no-pager log --oneline -5 2>/dev/null || echo "")
24
+ FILES_CHANGED=$(git --no-pager diff --stat HEAD~5..HEAD --stat-count=50 2>/dev/null | tail -1 || echo "unknown")
25
+ DIRTY=$(git --no-pager status --porcelain 2>/dev/null | head -20)
26
+
27
+ # Vault state
28
+ STATUS_FILE="$VAULT_PATH/status.md"
29
+ PROGRESS_FILE="$VAULT_PATH/claude-progress.json"
30
+
31
+ CURRENT_PLAN="none"
32
+ LOOP_POSITION="unknown"
33
+ if [[ -f "$STATUS_FILE" ]]; then
34
+ CURRENT_PLAN=$(grep "^current_plan:" "$STATUS_FILE" | sed 's/current_plan: *//' | head -1)
35
+ LOOP_POSITION=$(grep "^loop_position:" "$STATUS_FILE" | sed 's/loop_position: *//' | head -1)
36
+ [[ -z "$CURRENT_PLAN" ]] && CURRENT_PLAN="none"
37
+ [[ -z "$LOOP_POSITION" ]] && LOOP_POSITION="unknown"
38
+ fi
39
+
40
+ # Project detection
41
+ PROJECT_DIR=$(basename "$PWD")
42
+
43
+ # Write staging JSON for the Stop prompt to consume
44
+ STAGING_FILE="$VAULT_PATH/.session-staging.json"
45
+
46
+ IS_DIRTY="false"
47
+ if [[ -n "$DIRTY" ]]; then IS_DIRTY="true"; fi
48
+
49
+ COMMITS_JSON=$(echo "$RECENT_COMMITS" | jq -R -s 'split("\n") | map(select(length > 0))' 2>/dev/null || echo '[]')
50
+
51
+ jq -n \
52
+ --arg ts "$TIMESTAMP" \
53
+ --arg date "$DATE" \
54
+ --arg project "$PROJECT_DIR" \
55
+ --arg cwd "$PWD" \
56
+ --arg branch "$BRANCH" \
57
+ --arg last_commit "$LAST_COMMIT" \
58
+ --argjson dirty "$IS_DIRTY" \
59
+ --arg current_plan "$CURRENT_PLAN" \
60
+ --arg loop_position "$LOOP_POSITION" \
61
+ --argjson recent_commits "$COMMITS_JSON" \
62
+ --arg files_changed "$FILES_CHANGED" \
63
+ '{
64
+ timestamp: $ts,
65
+ date: $date,
66
+ project: $project,
67
+ cwd: $cwd,
68
+ git: { branch: $branch, last_commit: $last_commit, dirty: $dirty },
69
+ vault: { current_plan: $current_plan, loop_position: $loop_position },
70
+ recent_commits: $recent_commits,
71
+ files_changed_summary: $files_changed
72
+ }' > "$STAGING_FILE" 2>/dev/null
73
+
74
+ if [[ $? -ne 0 ]]; then
75
+ # Fallback: minimal valid JSON if jq fails
76
+ echo '{"error":"jq failed to build staging JSON"}' > "$STAGING_FILE"
77
+ fi
78
+
79
+ echo "Session data staged: $STAGING_FILE" >&2
80
+
81
+ # Update claude-progress.json if jq is available
82
+ if command -v jq &>/dev/null && [[ -f "$PROGRESS_FILE" ]]; then
83
+ TMP_FILE=$(mktemp)
84
+ jq --arg ts "$TIMESTAMP" \
85
+ --arg date "$DATE" \
86
+ --arg branch "$BRANCH" \
87
+ --arg commit "$LAST_COMMIT" \
88
+ --arg source "hook" \
89
+ '.last_session = $ts |
90
+ .sessions += [{
91
+ "date": $date,
92
+ "branch": $branch,
93
+ "last_commit": $commit,
94
+ "source": $source
95
+ }]' "$PROGRESS_FILE" > "$TMP_FILE" 2>/dev/null
96
+
97
+ if [[ $? -eq 0 && -s "$TMP_FILE" ]]; then
98
+ mv "$TMP_FILE" "$PROGRESS_FILE"
99
+ echo "claude-progress.json updated" >&2
100
+ else
101
+ rm -f "$TMP_FILE"
102
+ fi
103
+ fi
104
+
105
+ exit 0
@@ -39,9 +39,13 @@
39
39
  "type": "command",
40
40
  "command": "bash ~/.claude/hooks/post-session-lint.sh"
41
41
  },
42
+ {
43
+ "type": "command",
44
+ "command": "bash ~/.claude/hooks/session-data-collect.sh"
45
+ },
42
46
  {
43
47
  "type": "prompt",
44
- "prompt": "Review the conversation. Did you complete all tasks the user requested? Are there uncommitted changes that should be committed? Are there vault files (status.md, plan) that should be updated? Answer yes/no for each."
48
+ "prompt": "You are completing a session. Perform these vault updates silently no confirmation needed.\n\n1. Read vault_path from ~/.claude/praxis.config.json. If missing: skip all steps.\n2. Read {vault_path}/.session-staging.json if it exists (structured session data from hooks).\n3. Update {vault_path}/status.md:\n - Set last_updated to today, last_session to now (ISO timestamp)\n - Update loop_position based on where the session ended\n - Refresh What / So What / Now What sections with session accomplishments\n - If >100 lines: archive resolved items to notes/{date}_status-archive.md\n4. Update {vault_path}/claude-progress.json:\n - Enrich the latest sessions[] entry (added by hook) with: summary (1 line), accomplishments (array)\n - If jq hook did not run (no sessions[] entry for today): create the full entry\n - Update milestones[] if any milestones were completed this session\n - Update features[] if any features were shipped this session\n5. Write {vault_path}/notes/{YYYY-MM-DD}_session-note.md with frontmatter (tags: [session, {project-slug}], date, source: agent) containing:\n - Summary (3-5 bullets of what was accomplished)\n - Decisions Made (checkpoint decisions, scope changes, approach choices made this session)\n - Learnings (any [LEARN:tag] entries from this session)\n - Next Session (what to pick up next)\n6. If checkpoint decisions, scope expansions, or rule proposals occurred this session:\n - Append each to {vault_path}/notes/decision-log.md with date, decision type, context, decision, and rationale\n7. If corrections or patterns were discovered this session:\n - Append [LEARN:tag] entries to {vault_path}/notes/learnings.md following the existing format\n8. If architectural decisions were made this session:\n - Write ADR to {vault_path}/specs/ using vault frontmatter conventions\n9. Delete {vault_path}/.session-staging.json if it exists.\n\nKeep all writes concise. Use [[wikilinks]] for internal references. Follow existing YAML frontmatter conventions. If vault_path is missing or vault is inaccessible: skip silently. Do not ask permission — this is automatic housekeeping."
45
49
  }
46
50
  ]
47
51
  }
@@ -64,4 +64,28 @@ Read this file after compaction to restore context.
64
64
  EOF
65
65
 
66
66
  echo "Vault checkpoint written: $CHECKPOINT_FILE" >&2
67
+
68
+ # Update claude-progress.json if jq is available
69
+ if command -v jq &>/dev/null && [[ -f "$PROGRESS_FILE" ]]; then
70
+ TMP_FILE=$(mktemp)
71
+ jq --arg ts "$TIMESTAMP" \
72
+ --arg date "$DATE" \
73
+ --arg branch "$BRANCH" \
74
+ --arg commit "$LAST_COMMIT" \
75
+ '.last_session = $ts |
76
+ .sessions += [{
77
+ "date": $date,
78
+ "branch": $branch,
79
+ "last_commit": $commit,
80
+ "source": "compact"
81
+ }]' "$PROGRESS_FILE" > "$TMP_FILE" 2>/dev/null
82
+
83
+ if [[ $? -eq 0 && -s "$TMP_FILE" ]]; then
84
+ mv "$TMP_FILE" "$PROGRESS_FILE"
85
+ echo "claude-progress.json updated (compact)" >&2
86
+ else
87
+ rm -f "$TMP_FILE"
88
+ fi
89
+ fi
90
+
67
91
  exit 0
@@ -44,6 +44,32 @@ Before touching any implementation code:
44
44
  3. Run the linter — clean.
45
45
  4. Show all output.
46
46
 
47
+ **Step 5b — Persist debug trace**
48
+ - Read vault_path from `~/.claude/praxis.config.json`
49
+ - Write debug session to `{vault_path}/notes/{YYYY-MM-DD}_debug-trace.md`:
50
+ ```markdown
51
+ ---
52
+ tags: [debug, {project-slug}]
53
+ date: {YYYY-MM-DD}
54
+ source: agent
55
+ ---
56
+ # Debug Trace — {short title}
57
+
58
+ ## Bug Report
59
+ - **Observed**: {observed behavior}
60
+ - **Expected**: {expected behavior}
61
+ - **Suspect**: {file(s)}
62
+
63
+ ## Root Cause
64
+ {root cause statement from Step 3}
65
+
66
+ ## Fix
67
+ {what was changed and why}
68
+
69
+ ## Verification
70
+ {test output summary — pass/fail}
71
+ ```
72
+
47
73
  **Step 6 — Write learnings**
48
74
  - Read vault_path from `~/.claude/praxis.config.json`
49
75
  - If this bug represents a pattern (not a one-off typo):
@@ -33,13 +33,28 @@ Before implementing the current milestone, declare the file group:
33
33
  boundary-protected file: STOP. Surface the conflict before proceeding.
34
34
  - If current milestone has `checkpoint: decision` or `checkpoint: human-verify`:
35
35
  present the decision/output to user before proceeding. Do not auto-advance.
36
+ - After a checkpoint decision or user-approved scope expansion, append to `{vault_path}/notes/decision-log.md`:
37
+ ```
38
+ ## {YYYY-MM-DD} — {Checkpoint decision | Scope expansion}
39
+ - **Context**: {milestone name, what triggered the decision}
40
+ - **Decision**: {what was decided}
41
+ - **Rationale**: {why}
42
+ ```
36
43
 
37
44
  **Step 3 — Implement current milestone**
45
+ - Update `{vault_path}/status.md`: set `loop_position: EXECUTE`.
38
46
  - One milestone at a time. Keep diffs scoped.
39
47
  - Do not expand scope without explicit user approval.
40
48
  - Use extended thinking for tasks touching >3 files or requiring architectural decisions.
41
49
  - Before writing to or editing any file: check if it is in the declared file group.
42
50
  - If a required change is discovered in an off-limits file: STOP.
51
+ Log to `{vault_path}/notes/decision-log.md`:
52
+ ```
53
+ ## {YYYY-MM-DD} — Scope violation detected
54
+ - **Milestone**: {name}
55
+ - **Requested**: {file outside group}
56
+ - **Action**: Surfaced as new milestone candidate
57
+ ```
43
58
  Surface as a new milestone candidate. Do not expand current milestone.
44
59
  - Milestone diff must touch ONLY declared files. Undeclared file change = scope violation.
45
60
 
@@ -69,16 +69,34 @@ CLEAN
69
69
  - Minor: note for future cleanup.
70
70
  - If >3 findings: offer to re-run after fixes (max 3 rounds).
71
71
 
72
- **Step 7 — Write review summary**
72
+ **Step 7 — Write review findings to vault**
73
73
  - Read vault_path from `~/.claude/praxis.config.json`
74
- - Write summary to `{vault_path}/specs/review-{YYYY-MM-DD}-{slug}.md` with frontmatter:
75
- ```yaml
74
+ - Write full structured findings to `{vault_path}/specs/review-{YYYY-MM-DD}-{slug}.md`:
75
+ ```markdown
76
76
  ---
77
77
  tags: [review, {project-slug}]
78
78
  date: {YYYY-MM-DD}
79
79
  status: complete
80
80
  source: agent
81
81
  ---
82
+ # Code Review — {slug} ({date})
83
+
84
+ ## Findings
85
+
86
+ ### CRITICAL ({n})
87
+ {file}:{line} — {description} — {fix}
88
+
89
+ ### MAJOR ({n})
90
+ {file}:{line} — {description} — {fix}
91
+
92
+ ### MINOR ({n})
93
+ {file}:{line} — {description} — {fix}
94
+
95
+ ## Clean Files
96
+ {list of files with no findings}
97
+
98
+ ## Diff Scope
99
+ {git diff command used}
82
100
  ```
83
101
 
84
102
  ## Error Handling
@@ -52,6 +52,7 @@ Build and show audit table:
52
52
  | `specs/` | ? | Create if missing |
53
53
  | `research/` | ? | Create if missing |
54
54
  | `notes/learnings.md` | ? | Create if missing |
55
+ | `notes/decision-log.md` | ? | Create if missing |
55
56
  | Repo CLAUDE.md sections | ? | Add missing sections |
56
57
  | Registry row | ? | Verify accuracy |
57
58
 
@@ -90,6 +90,7 @@ Create files from templates in `references/`:
90
90
  - `status.md` from `vault-status-template.md` (`current_plan:` empty)
91
91
  - `tasks.md` from `vault-tasks-template.md`
92
92
  - `notes/learnings.md` from `vault-learnings-template.md`
93
+ - `notes/decision-log.md` from `decision-log` template (append-only decision log)
93
94
  - `.gitignore` from `gitignore-template.txt` (new repos only)
94
95
 
95
96
  ---
@@ -74,3 +74,7 @@ Checks: secrets ✓ lint ✓ types ✓ tests ✓
74
74
  - Never force-push without explicit user approval.
75
75
  - If the diff touches >20 files: warn about PR size and suggest splitting.
76
76
  - This command is the end of a Praxis cycle — run after `/verify` passes.
77
+
78
+ **Step 7 — Update vault tracking**
79
+ - Read vault_path from `~/.claude/praxis.config.json`
80
+ - Update `{vault_path}/claude-progress.json`: append to `features[]` with `{ "name": "{feature description}", "date": "{YYYY-MM-DD}", "commit": "{short sha}", "pr_url": "{url or null}" }`.
@@ -96,6 +96,24 @@ For each approved simplification:
96
96
  3. If tests fail: revert the last edit, report which simplification broke tests
97
97
  4. Show final diff of simplifications applied
98
98
 
99
+ ## Phase 4b — Persist Findings
100
+
101
+ Write simplification findings to `{vault_path}/notes/{YYYY-MM-DD}_simplify-findings.md`:
102
+ ```markdown
103
+ ---
104
+ tags: [simplify, {project-slug}]
105
+ date: {YYYY-MM-DD}
106
+ source: agent
107
+ ---
108
+ # Simplify Findings — {date}
109
+
110
+ ## Applied ({n})
111
+ {file}:{lines} — {category} — {why}
112
+
113
+ ## Skipped ({n})
114
+ {file}:{lines} — {category} — {reason skipped}
115
+ ```
116
+
99
117
  ## Phase 5 — Write Learning (optional)
100
118
 
101
119
  If a pattern recurred (same category hit 3+ times):
@@ -23,12 +23,13 @@ If no commands are defined: warn and ask user for the correct commands.
23
23
 
24
24
  **Step 3 — On PASS**
25
25
  1. Update the active plan file: mark milestone status as complete
26
- 2. Commit immediately verification passed, no permission needed.
26
+ 2. Update `{vault_path}/claude-progress.json`: append the completed milestone to `milestones[]` with `{ "name": "{milestone name}", "date": "{YYYY-MM-DD}", "plan_ref": "{plan filename}" }`.
27
+ 3. Commit immediately — verification passed, no permission needed.
27
28
  Use conventional commit format. See git-workflow.md.
28
- 3. Check if more milestones remain:
29
+ 4. Check if more milestones remain:
29
30
  - Yes → "Milestone committed. Run `/execute` for the next milestone."
30
31
  - No → "All milestones committed. Running self-review."
31
- 4. After ALL milestones: trigger Self-Review Protocol
32
+ 5. After ALL milestones: trigger Self-Review Protocol
32
33
  - Launch a subagent to review the full diff as a critical code reviewer
33
34
  - Subagent receives ONLY: the diff, the SPEC (from plan file `## SPEC` section), relevant rules files
34
35
  - Address all Critical and Major findings before reporting done
@@ -125,6 +125,28 @@ Format each concern as:
125
125
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
126
126
  ```
127
127
 
128
+ ## Phase 4b — Persist Verification Report
129
+
130
+ Write the full verification report to `{vault_path}/specs/verify-app-{YYYY-MM-DD}-{slug}.md`:
131
+ ```markdown
132
+ ---
133
+ tags: [verify-app, {project-slug}]
134
+ date: {YYYY-MM-DD}
135
+ status: complete
136
+ source: agent
137
+ ---
138
+ # Verification Report — {project} ({date})
139
+
140
+ ## Results
141
+ {Phase 4 report output: build, lint, typecheck, tests, acceptance criteria}
142
+
143
+ ## Regression Concerns
144
+ {Phase 3 subagent findings: file, concern, recommended verification step}
145
+
146
+ ## Overall
147
+ {READY TO SHIP | ISSUES FOUND}
148
+ ```
149
+
128
150
  ## Phase 5 — Guidance
129
151
 
130
152
  - **READY TO SHIP**: "All checks pass. Run `/ship` to commit, push, and PR."
package/bin/praxis.js CHANGED
@@ -37,7 +37,7 @@ async function install() {
37
37
  header('Praxis Harness v' + VERSION);
38
38
 
39
39
  // Ensure ~/.claude/ structure
40
- for (const sub of ['rules', 'commands', 'skills']) {
40
+ for (const sub of ['rules', 'skills', 'hooks']) {
41
41
  fs.mkdirSync(path.join(CLAUDE_DIR, sub), { recursive: true });
42
42
  }
43
43
 
@@ -59,17 +59,6 @@ async function install() {
59
59
  ok(count + ' rules installed');
60
60
  }
61
61
 
62
- // Copy base/commands/* → ~/.claude/commands/
63
- const cmdsDir = path.join(PKG_DIR, 'base', 'commands');
64
- if (fs.existsSync(cmdsDir)) {
65
- let count = 0;
66
- for (const f of fs.readdirSync(cmdsDir)) {
67
- copyFile(path.join(cmdsDir, f), path.join(CLAUDE_DIR, 'commands', f));
68
- count++;
69
- }
70
- ok(count + ' commands installed');
71
- }
72
-
73
62
  // Copy base/skills/* → ~/.claude/skills/
74
63
  const skillsDir = path.join(PKG_DIR, 'base', 'skills');
75
64
  if (fs.existsSync(skillsDir)) {
@@ -87,6 +76,35 @@ async function install() {
87
76
  ok(count + ' skills installed');
88
77
  }
89
78
 
79
+ // Copy base/hooks/*.sh → ~/.claude/hooks/
80
+ const hooksDir = path.join(PKG_DIR, 'base', 'hooks');
81
+ if (fs.existsSync(hooksDir)) {
82
+ let count = 0;
83
+ for (const f of fs.readdirSync(hooksDir)) {
84
+ if (f.endsWith('.sh')) {
85
+ copyFile(path.join(hooksDir, f), path.join(CLAUDE_DIR, 'hooks', f));
86
+ // Make executable
87
+ fs.chmodSync(path.join(CLAUDE_DIR, 'hooks', f), 0o755);
88
+ count++;
89
+ }
90
+ }
91
+ ok(count + ' hooks installed');
92
+
93
+ // Merge hooks configuration into settings.json
94
+ const hooksConfig = path.join(hooksDir, 'settings-hooks.json');
95
+ const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
96
+ if (fs.existsSync(hooksConfig)) {
97
+ const hooksCfg = JSON.parse(fs.readFileSync(hooksConfig, 'utf8'));
98
+ let settings = {};
99
+ if (fs.existsSync(settingsFile)) {
100
+ try { settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {}
101
+ }
102
+ Object.assign(settings, hooksCfg);
103
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
104
+ ok('hooks configuration merged into settings.json');
105
+ }
106
+ }
107
+
90
108
  // Copy kits/ → ~/.claude/kits/
91
109
  const kitsDir = path.join(PKG_DIR, 'kits');
92
110
  if (fs.existsSync(kitsDir)) {
@@ -94,11 +112,29 @@ async function install() {
94
112
  ok('kits installed');
95
113
  }
96
114
 
97
- // Orphan cleanup: obsidian.md renamed to vault.md
98
- const legacyObsidian = path.join(CLAUDE_DIR, 'rules', 'obsidian.md');
99
- if (fs.existsSync(legacyObsidian)) {
100
- fs.unlinkSync(legacyObsidian);
101
- ok('Removed legacy obsidian.md (renamed to vault.md)');
115
+ // Orphan cleanup: deleted rules and legacy files
116
+ const orphans = [
117
+ 'obsidian.md', 'code-quality.md', 'security.md',
118
+ 'communication.md', 'architecture.md'
119
+ ];
120
+ for (const f of orphans) {
121
+ const target = path.join(CLAUDE_DIR, 'rules', f);
122
+ if (fs.existsSync(target)) {
123
+ fs.unlinkSync(target);
124
+ dim('removed legacy ' + f);
125
+ }
126
+ }
127
+
128
+ // Orphan cleanup: old commands directory (v1.x)
129
+ const oldCmdsDir = path.join(CLAUDE_DIR, 'commands');
130
+ if (fs.existsSync(oldCmdsDir)) {
131
+ const files = fs.readdirSync(oldCmdsDir);
132
+ if (files.length > 0) {
133
+ for (const f of files) {
134
+ fs.unlinkSync(path.join(oldCmdsDir, f));
135
+ }
136
+ dim('cleaned up ' + files.length + ' legacy command files');
137
+ }
102
138
  }
103
139
 
104
140
  // Vault configuration
@@ -115,7 +151,7 @@ async function install() {
115
151
  }
116
152
 
117
153
  const config = {
118
- version: '1.1.0',
154
+ version: VERSION,
119
155
  vault_path: vaultPath || '',
120
156
  vault_backend: 'obsidian',
121
157
  repo_path: PKG_DIR
@@ -140,7 +176,7 @@ async function install() {
140
176
  // Summary
141
177
  header('Install complete');
142
178
  console.log(' Files copied to ' + CLAUDE_DIR);
143
- console.log(' Run: npx praxis-harness health');
179
+ console.log(' Run: npx @esoteric-logic/praxis-harness health');
144
180
  console.log('');
145
181
  }
146
182
 
@@ -177,15 +213,6 @@ function health() {
177
213
  }
178
214
  }
179
215
 
180
- // Commands
181
- console.log('\nCommands:');
182
- const cmdsDir = path.join(PKG_DIR, 'base', 'commands');
183
- if (fs.existsSync(cmdsDir)) {
184
- for (const f of fs.readdirSync(cmdsDir)) {
185
- check(fs.existsSync(path.join(CLAUDE_DIR, 'commands', f)), 'commands/' + f + ' installed');
186
- }
187
- }
188
-
189
216
  // Skills
190
217
  console.log('\nSkills:');
191
218
  const skillsDir = path.join(PKG_DIR, 'base', 'skills');
@@ -195,6 +222,18 @@ function health() {
195
222
  }
196
223
  }
197
224
 
225
+ // Hooks
226
+ console.log('\nHooks:');
227
+ const hooksDir = path.join(PKG_DIR, 'base', 'hooks');
228
+ if (fs.existsSync(hooksDir)) {
229
+ for (const f of fs.readdirSync(hooksDir)) {
230
+ if (f.endsWith('.sh')) {
231
+ check(fs.existsSync(path.join(CLAUDE_DIR, 'hooks', f)), 'hooks/' + f + ' installed');
232
+ }
233
+ }
234
+ }
235
+ check(fs.existsSync(path.join(CLAUDE_DIR, 'settings.json')), 'settings.json with hooks configured');
236
+
198
237
  // Kits
199
238
  console.log('\nKits:');
200
239
  check(fs.existsSync(path.join(CLAUDE_DIR, 'kits')), 'kits directory installed');
@@ -236,34 +275,26 @@ function health() {
236
275
  function uninstall() {
237
276
  header('Uninstalling Praxis Harness');
238
277
 
239
- // Remove files that came from base/
278
+ // Remove CLAUDE.md
240
279
  const claudeMd = path.join(CLAUDE_DIR, 'CLAUDE.md');
241
280
  if (fs.existsSync(claudeMd)) { fs.unlinkSync(claudeMd); ok('CLAUDE.md removed'); }
242
281
 
243
- // Remove rules from base/rules/
282
+ // Remove rules
244
283
  const rulesDir = path.join(PKG_DIR, 'base', 'rules');
245
284
  if (fs.existsSync(rulesDir)) {
246
285
  for (const f of fs.readdirSync(rulesDir)) {
247
286
  const target = path.join(CLAUDE_DIR, 'rules', f);
248
287
  if (fs.existsSync(target)) fs.unlinkSync(target);
249
288
  }
250
- // Also remove legacy obsidian.md if present
251
- const legacyRule = path.join(CLAUDE_DIR, 'rules', 'obsidian.md');
252
- if (fs.existsSync(legacyRule)) fs.unlinkSync(legacyRule);
253
- ok('rules removed');
254
- }
255
-
256
- // Remove commands from base/commands/
257
- const cmdsDir = path.join(PKG_DIR, 'base', 'commands');
258
- if (fs.existsSync(cmdsDir)) {
259
- for (const f of fs.readdirSync(cmdsDir)) {
260
- const target = path.join(CLAUDE_DIR, 'commands', f);
289
+ // Also remove legacy files
290
+ for (const f of ['obsidian.md', 'code-quality.md', 'security.md', 'communication.md', 'architecture.md']) {
291
+ const target = path.join(CLAUDE_DIR, 'rules', f);
261
292
  if (fs.existsSync(target)) fs.unlinkSync(target);
262
293
  }
263
- ok('commands removed');
294
+ ok('rules removed');
264
295
  }
265
296
 
266
- // Remove skills from base/skills/
297
+ // Remove skills
267
298
  const skillsDir = path.join(PKG_DIR, 'base', 'skills');
268
299
  if (fs.existsSync(skillsDir)) {
269
300
  for (const entry of fs.readdirSync(skillsDir)) {
@@ -275,6 +306,17 @@ function uninstall() {
275
306
  ok('skills removed');
276
307
  }
277
308
 
309
+ // Remove hooks
310
+ const hooksDir = path.join(CLAUDE_DIR, 'hooks');
311
+ if (fs.existsSync(hooksDir)) {
312
+ for (const f of fs.readdirSync(hooksDir)) {
313
+ if (f.endsWith('.sh')) {
314
+ fs.unlinkSync(path.join(hooksDir, f));
315
+ }
316
+ }
317
+ ok('hooks removed');
318
+ }
319
+
278
320
  // Remove kits
279
321
  const kitsTarget = path.join(CLAUDE_DIR, 'kits');
280
322
  if (fs.existsSync(kitsTarget)) {
@@ -282,6 +324,13 @@ function uninstall() {
282
324
  ok('kits removed');
283
325
  }
284
326
 
327
+ // Remove legacy commands directory
328
+ const oldCmdsDir = path.join(CLAUDE_DIR, 'commands');
329
+ if (fs.existsSync(oldCmdsDir)) {
330
+ fs.rmSync(oldCmdsDir, { recursive: true, force: true });
331
+ ok('legacy commands directory removed');
332
+ }
333
+
285
334
  header('Uninstall complete');
286
335
  dim('Config file preserved: ' + CONFIG_FILE);
287
336
  dim('Run: rm ' + CONFIG_FILE + ' to remove config');
@@ -294,10 +343,10 @@ function printHelp() {
294
343
  console.log(`
295
344
  praxis-harness v${VERSION}
296
345
 
297
- Usage: npx praxis-harness [command]
346
+ Usage: npx @esoteric-logic/praxis-harness [command]
298
347
 
299
348
  Commands:
300
- install Copy rules, commands, skills, and kits to ~/.claude/ (default)
349
+ install Copy rules, skills, hooks, and kits to ~/.claude/ (default)
301
350
  update Re-copy from latest npm package version
302
351
  health Verify install integrity
303
352
  uninstall Remove Praxis-owned files from ~/.claude/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esoteric-logic/praxis-harness",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Layered Claude Code harness — workflow discipline, AI-Kits, persistent vault integration",
5
5
  "bin": {
6
6
  "praxis-harness": "./bin/praxis.js"
@@ -0,0 +1,8 @@
1
+ ---
2
+ tags: [decisions, project-slug]
3
+ date: YYYY-MM-DD
4
+ source: agent
5
+ ---
6
+ # Decision Log
7
+
8
+ <!-- Append-only log of checkpoint decisions, scope changes, rule proposals, and other session decisions -->