continuous-improvement 1.1.0 → 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/bin/analyze.sh ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # analyze.sh — Read observations.jsonl, detect patterns, append rules to rules.md
3
+ # This is the actual analysis pipeline. Runs via /continuous-improvement or observer-loop.
4
+ # Uses claude CLI with Haiku for cost-efficient analysis.
5
+
6
+ set -euo pipefail
7
+
8
+ MULAHAZAH_DIR="${HOME}/.claude/mulahazah"
9
+ RULES_FILE="${MULAHAZAH_DIR}/rules.md"
10
+ GLOBAL_OBS="${MULAHAZAH_DIR}/observations.jsonl"
11
+
12
+ # Detect project
13
+ PROJECT_DIR=""
14
+ PROJECT_OBS=""
15
+ if REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then
16
+ PROJECT_HASH=$(printf '%s' "$REPO_ROOT" | sha256sum | cut -c1-12)
17
+ PROJECT_DIR="${MULAHAZAH_DIR}/projects/${PROJECT_HASH}"
18
+ PROJECT_OBS="${PROJECT_DIR}/observations.jsonl"
19
+ fi
20
+
21
+ # Find observations file
22
+ OBS_FILE=""
23
+ if [ -n "$PROJECT_OBS" ] && [ -f "$PROJECT_OBS" ]; then
24
+ OBS_FILE="$PROJECT_OBS"
25
+ elif [ -f "$GLOBAL_OBS" ]; then
26
+ OBS_FILE="$GLOBAL_OBS"
27
+ fi
28
+
29
+ if [ -z "$OBS_FILE" ] || [ ! -s "$OBS_FILE" ]; then
30
+ echo "No observations found. Use Claude Code with hooks installed to generate observations."
31
+ exit 0
32
+ fi
33
+
34
+ OBS_COUNT=$(wc -l < "$OBS_FILE")
35
+ echo "Found $OBS_COUNT observations in $OBS_FILE"
36
+
37
+ if [ "$OBS_COUNT" -lt 5 ]; then
38
+ echo "Need at least 5 observations for meaningful analysis. Keep using Claude Code."
39
+ exit 0
40
+ fi
41
+
42
+ # Read existing rules to avoid duplicates
43
+ EXISTING_RULES=""
44
+ if [ -f "$RULES_FILE" ]; then
45
+ EXISTING_RULES=$(cat "$RULES_FILE")
46
+ fi
47
+
48
+ # Take last 200 observations (keep prompt size manageable)
49
+ RECENT_OBS=$(tail -200 "$OBS_FILE")
50
+
51
+ # Build analysis prompt
52
+ ANALYSIS_PROMPT="Analyze these Claude Code session observations and extract behavioral patterns.
53
+
54
+ OBSERVATIONS (JSONL — each line is a tool call):
55
+ ${RECENT_OBS}
56
+
57
+ EXISTING RULES (already learned — do NOT duplicate these):
58
+ ${EXISTING_RULES}
59
+
60
+ YOUR TASK:
61
+ 1. Look for REPEATED PATTERNS — same tool sequence used 3+ times
62
+ 2. Look for ERROR-THEN-FIX sequences — tool fails, next tools fix it
63
+ 3. Look for TOOL PREFERENCES — one tool consistently chosen over alternatives
64
+ 4. Look for WORKFLOW PATTERNS — consistent ordering of operations
65
+
66
+ OUTPUT FORMAT — output ONLY new rules, one per line, as markdown list items:
67
+ - Rule: [specific actionable rule based on observed pattern]
68
+
69
+ Rules must be:
70
+ - Specific and actionable (not vague advice)
71
+ - Based on actual observed patterns (cite the tools/sequence you saw)
72
+ - Different from existing rules listed above
73
+ - Useful for future sessions
74
+
75
+ If no new patterns are found, output exactly: NO_NEW_PATTERNS
76
+
77
+ Output ONLY the rules list or NO_NEW_PATTERNS. No explanation, no preamble."
78
+
79
+ # Run analysis with Haiku
80
+ echo "Analyzing patterns with Haiku..."
81
+ RESULT=$(echo "$ANALYSIS_PROMPT" | claude --model haiku --print -p - 2>/dev/null) || {
82
+ echo "Analysis failed — claude CLI error. Try running manually."
83
+ exit 1
84
+ }
85
+
86
+ if [ "$RESULT" = "NO_NEW_PATTERNS" ] || [ -z "$RESULT" ]; then
87
+ echo "No new patterns detected yet. Keep using Claude Code — patterns emerge over time."
88
+ exit 0
89
+ fi
90
+
91
+ # Append new rules to rules.md
92
+ echo "" >> "$RULES_FILE" 2>/dev/null || true
93
+ mkdir -p "$(dirname "$RULES_FILE")"
94
+ {
95
+ if [ ! -f "$RULES_FILE" ]; then
96
+ echo "# Learned Rules"
97
+ echo ""
98
+ echo "Rules extracted from session observations by Mulahazah."
99
+ echo "Remove any rule that causes problems. Keep what helps."
100
+ echo ""
101
+ echo "---"
102
+ echo ""
103
+ fi
104
+ echo "## $(date +%Y-%m-%d) analysis"
105
+ echo ""
106
+ echo "$RESULT"
107
+ echo ""
108
+ } >> "$RULES_FILE"
109
+
110
+ # Count new rules
111
+ NEW_COUNT=$(echo "$RESULT" | grep -c "^- " || true)
112
+ echo ""
113
+ echo "Added $NEW_COUNT new rules to $RULES_FILE"
114
+ echo ""
115
+ echo "$RESULT"
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * continuous-improvement installer
5
+ *
6
+ * Usage:
7
+ * npx continuous-improvement install # auto-detect & install
8
+ * npx continuous-improvement install --target claude # install to ~/.claude/skills/ + Mulahazah
9
+ * npx continuous-improvement install --target openclaw # install to ~/.openclaw/skills/
10
+ * npx continuous-improvement install --target cursor # install to ~/.cursor/skills/
11
+ * npx continuous-improvement install --target all # install to all detected targets
12
+ * npx continuous-improvement install --uninstall # remove from all targets
13
+ */
14
+
15
+ import {
16
+ existsSync,
17
+ mkdirSync,
18
+ copyFileSync,
19
+ readFileSync,
20
+ writeFileSync,
21
+ rmSync,
22
+ chmodSync,
23
+ } from "node:fs";
24
+ import { join, dirname } from "node:path";
25
+ import { homedir } from "node:os";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+ const SKILL_SOURCE = join(__dirname, "..", "SKILL.md");
31
+ const SKILL_NAME = "continuous-improvement";
32
+ const REPO_ROOT = join(__dirname, "..");
33
+
34
+ const TARGETS = {
35
+ claude: {
36
+ label: "Claude Code",
37
+ dir: join(homedir(), ".claude", "skills", SKILL_NAME),
38
+ },
39
+ openclaw: {
40
+ label: "OpenClaw",
41
+ dir: join(homedir(), ".openclaw", "skills", SKILL_NAME),
42
+ },
43
+ cursor: {
44
+ label: "Cursor",
45
+ dir: join(homedir(), ".cursor", "skills", SKILL_NAME),
46
+ },
47
+ codex: {
48
+ label: "Codex",
49
+ dir: join(homedir(), ".codex", "skills", SKILL_NAME),
50
+ },
51
+ };
52
+
53
+ function detectTargets() {
54
+ const detected = [];
55
+ for (const [key, target] of Object.entries(TARGETS)) {
56
+ const parentDir = dirname(target.dir);
57
+ const configDir = dirname(parentDir);
58
+ if (existsSync(configDir)) {
59
+ detected.push(key);
60
+ }
61
+ }
62
+ return detected;
63
+ }
64
+
65
+ function installTo(key) {
66
+ const target = TARGETS[key];
67
+ if (!target) {
68
+ console.error(` Unknown target: ${key}`);
69
+ return false;
70
+ }
71
+
72
+ try {
73
+ mkdirSync(target.dir, { recursive: true });
74
+ copyFileSync(SKILL_SOURCE, join(target.dir, "SKILL.md"));
75
+ console.log(` ✓ ${target.label} → ${target.dir}/SKILL.md`);
76
+
77
+ if (key === "claude") {
78
+ setupMulahazah();
79
+ }
80
+
81
+ return true;
82
+ } catch (err) {
83
+ console.error(` ✗ ${target.label}: ${err.message}`);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function setupMulahazah() {
89
+ const home = homedir();
90
+ const instinctsDir = join(home, ".claude", "instincts");
91
+ const globalDir = join(instinctsDir, "global");
92
+
93
+ // 1. Create directory structure
94
+ mkdirSync(globalDir, { recursive: true });
95
+ console.log(` ✓ Instincts dir → ${instinctsDir}/`);
96
+
97
+ // 2. Copy observe.sh and make executable
98
+ const observeSrc = join(REPO_ROOT, "hooks", "observe.sh");
99
+ const observeDest = join(instinctsDir, "observe.sh");
100
+ if (existsSync(observeSrc)) {
101
+ copyFileSync(observeSrc, observeDest);
102
+ chmodSync(observeDest, 0o755);
103
+ console.log(` ✓ observe.sh → ${observeDest}`);
104
+ }
105
+
106
+ // 3. Copy /continuous-improvement command
107
+ const commandsDir = join(home, ".claude", "commands");
108
+ mkdirSync(commandsDir, { recursive: true });
109
+ const cmdSrc = join(REPO_ROOT, "commands", "continuous-improvement.md");
110
+ const cmdDest = join(commandsDir, "continuous-improvement.md");
111
+ if (existsSync(cmdSrc)) {
112
+ copyFileSync(cmdSrc, cmdDest);
113
+ console.log(` ✓ /continuous-improvement command → ${cmdDest}`);
114
+ }
115
+
116
+ // 4. Patch ~/.claude/settings.json with hooks
117
+ patchClaudeSettings(observeDest);
118
+ }
119
+
120
+ function patchClaudeSettings(observePath) {
121
+ const settingsPath = join(homedir(), ".claude", "settings.json");
122
+
123
+ let settings = {};
124
+ if (existsSync(settingsPath)) {
125
+ try {
126
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
127
+ } catch {
128
+ console.warn(` ! Could not parse ${settingsPath} — skipping hook patch`);
129
+ return;
130
+ }
131
+ }
132
+
133
+ if (!settings.hooks) settings.hooks = {};
134
+
135
+ const hookEntry = {
136
+ matcher: "",
137
+ hooks: [
138
+ {
139
+ type: "command",
140
+ command: `bash "${observePath}"`,
141
+ },
142
+ ],
143
+ };
144
+
145
+ let changed = false;
146
+
147
+ for (const hookType of ["PreToolUse", "PostToolUse"]) {
148
+ if (!Array.isArray(settings.hooks[hookType])) {
149
+ settings.hooks[hookType] = [];
150
+ }
151
+ const alreadyPatched = settings.hooks[hookType].some(
152
+ (h) =>
153
+ Array.isArray(h.hooks) &&
154
+ h.hooks.some((hh) => hh.command && hh.command.includes("observe.sh"))
155
+ );
156
+ if (!alreadyPatched) {
157
+ settings.hooks[hookType].push(hookEntry);
158
+ changed = true;
159
+ }
160
+ }
161
+
162
+ if (changed) {
163
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
164
+ console.log(` ✓ Patched ~/.claude/settings.json with PreToolUse/PostToolUse hooks`);
165
+ } else {
166
+ console.log(` ✓ settings.json already has observe.sh hooks — no change needed`);
167
+ }
168
+ }
169
+
170
+ function uninstallAll() {
171
+ console.log("\nUninstalling continuous-improvement skill...\n");
172
+ let removed = 0;
173
+ for (const [key, target] of Object.entries(TARGETS)) {
174
+ const skillFile = join(target.dir, "SKILL.md");
175
+ if (existsSync(skillFile)) {
176
+ try {
177
+ rmSync(target.dir, { recursive: true });
178
+ console.log(` ✓ Removed from ${target.label}`);
179
+ removed++;
180
+ } catch (err) {
181
+ console.error(` ✗ ${target.label}: ${err.message}`);
182
+ }
183
+ }
184
+ }
185
+ if (removed === 0) {
186
+ console.log(" No installations found.");
187
+ }
188
+ console.log();
189
+ }
190
+
191
+ function printUsage() {
192
+ console.log(`
193
+ Usage: npx continuous-improvement install [options]
194
+
195
+ Options:
196
+ --target <name> Install to specific target (claude, openclaw, cursor, codex, all)
197
+ --uninstall Remove from all targets
198
+ --help Show this help
199
+
200
+ Examples:
201
+ npx continuous-improvement install # auto-detect & install
202
+ npx continuous-improvement install --target all # install everywhere
203
+ npx continuous-improvement install --uninstall # remove all
204
+ `);
205
+ }
206
+
207
+ // --- Main ---
208
+
209
+ const args = process.argv.slice(2);
210
+
211
+ if (args.includes("--help") || args.includes("-h")) {
212
+ printUsage();
213
+ process.exit(0);
214
+ }
215
+
216
+ if (args.includes("--uninstall")) {
217
+ uninstallAll();
218
+ process.exit(0);
219
+ }
220
+
221
+ console.log(`
222
+ continuous-improvement v2.1
223
+ Research → Plan → Execute → Verify → Reflect → Learn → Iterate
224
+ `);
225
+
226
+ const targetIdx = args.indexOf("--target");
227
+ let targets;
228
+
229
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
230
+ const requested = args[targetIdx + 1].toLowerCase();
231
+ if (requested === "all") {
232
+ targets = Object.keys(TARGETS);
233
+ } else if (TARGETS[requested]) {
234
+ targets = [requested];
235
+ } else {
236
+ console.error(`Unknown target: ${requested}`);
237
+ console.error(`Available: ${Object.keys(TARGETS).join(", ")}, all`);
238
+ process.exit(1);
239
+ }
240
+ } else {
241
+ targets = detectTargets();
242
+ if (targets.length === 0) {
243
+ console.log("No supported agent configs detected. Installing to Claude Code by default.\n");
244
+ targets = ["claude"];
245
+ } else {
246
+ console.log(`Detected: ${targets.map((t) => TARGETS[t].label).join(", ")}\n`);
247
+ }
248
+ }
249
+
250
+ console.log("Installing...\n");
251
+
252
+ let installed = 0;
253
+ for (const t of targets) {
254
+ if (installTo(t)) installed++;
255
+ }
256
+
257
+ const hasClaude = targets.includes("claude");
258
+
259
+ console.log(`
260
+ ${installed > 0 ? "Done." : "Failed."} Installed to ${installed}/${targets.length} target(s).
261
+ ${hasClaude ? "\nHooks are capturing. System auto-levels as you use it." : ""}
262
+ Next steps:
263
+ 1. Start a new Claude Code session
264
+ 2. Say: "Use the continuous-improvement framework to [your task]"
265
+ 3. After your first task, run: /continuous-improvement
266
+ `);
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: continuous-improvement
3
+ description: "Reflect on the current session, analyze observations for patterns, and show instinct status. Run after finishing significant work."
4
+ ---
5
+
6
+ # /continuous-improvement
7
+
8
+ Run this after completing significant work. It does three things in order.
9
+
10
+ ## Step 1: Reflect
11
+
12
+ Generate a reflection for this session based on what happened:
13
+
14
+ ```
15
+ ## Reflection — [Date]
16
+ - What worked:
17
+ - What failed:
18
+ - What I'd do differently:
19
+ - Rule to add:
20
+ ```
21
+
22
+ If there's a "Rule to add", create an instinct YAML file with 0.6 starting confidence in the project's instinct directory.
23
+
24
+ ## Step 2: Analyze Observations
25
+
26
+ Check `~/.claude/instincts/` for the current project (detect via git root → SHA-256 first 12 chars).
27
+
28
+ Look at `~/.claude/instincts/<hash>/observations.jsonl`. If 20+ lines exist:
29
+
30
+ 1. Read the last 500 lines
31
+ 2. Read existing instinct `*.yaml` files (project + global)
32
+ 3. Detect patterns:
33
+ - User corrections → "don't do X" instincts
34
+ - Error→fix sequences → "when X fails, try Y"
35
+ - Repeated workflows (3+ times) → "for X, do A→B→C"
36
+ - Tool preferences → "use tool Y for task X"
37
+ 4. Create/update instinct YAML files
38
+ 5. Be conservative: only create instincts for 3+ observations of the same pattern
39
+
40
+ If fewer than 20 observations, skip analysis and note the count.
41
+
42
+ ## Step 3: Show Status
43
+
44
+ Display all instincts for the current project + global:
45
+
46
+ ```
47
+ === continuous-improvement ===
48
+
49
+ ## Level: [CAPTURE | ANALYZE | SUGGEST | AUTO-APPLY]
50
+
51
+ ## Session Reflection
52
+ - What worked: [from this session]
53
+ - What failed: [from this session]
54
+ - What I'd do differently: [from this session]
55
+ - Rule to add: [captured as instinct]
56
+
57
+ ## Learning
58
+ NEW [instinct-id] [domain] [confidence] (from reflection)
59
+ ↑ [instinct-id] [domain] [old]→[new] (+N observations)
60
+
61
+ ## Instincts — [project-name] ([hash])
62
+ ● [0.85] instinct-id domain auto-apply
63
+ ◐ [0.60] instinct-id domain suggest
64
+ ○ [0.35] instinct-id domain silent
65
+
66
+ ## Instincts — global
67
+ ● [0.90] instinct-id domain auto-apply
68
+
69
+ ## Next
70
+ - Keep working — hooks capture automatically
71
+ - System auto-levels as instincts gain confidence
72
+ ```
73
+
74
+ If no instincts or observations exist yet, explain this is expected — the system is in CAPTURE level and will create instincts after 20+ observations accumulate.
package/hooks/observe.sh CHANGED
@@ -6,12 +6,7 @@
6
6
  # Always exit 0 — never block the Claude session
7
7
  trap 'exit 0' EXIT ERR INT TERM
8
8
 
9
- # Require jq — if unavailable, silently exit
10
- command -v jq &>/dev/null || exit 0
11
-
12
- MULAHAZAH_DIR="${HOME}/.claude/mulahazah"
13
- PROJECTS_DIR="${MULAHAZAH_DIR}/projects"
14
- GLOBAL_REGISTRY="${MULAHAZAH_DIR}/projects.json"
9
+ INSTINCTS_DIR="${HOME}/.claude/instincts"
15
10
 
16
11
  # ---------------------------------------------------------------------------
17
12
  # Read stdin (hook payload) — single read for performance
@@ -20,17 +15,29 @@ INPUT="$(cat)"
20
15
  [[ -z "$INPUT" ]] && exit 0
21
16
 
22
17
  # ---------------------------------------------------------------------------
23
- # Parse hook payload in one jq call
18
+ # Parse hook payload use jq if available, otherwise basic extraction
24
19
  # ---------------------------------------------------------------------------
25
- read -r TOOL_NAME SESSION_ID HAS_OUTPUT INPUT_JSON OUTPUT_JSON <<< "$(
26
- printf '%s' "$INPUT" | jq -r '
27
- (.tool_name // ""),
28
- (.session_id // ""),
29
- (if has("tool_output") then "yes" else "no" end),
30
- ((.tool_input // {} | tostring) | .[0:500]),
31
- ((.tool_output // {} | tostring) | .[0:200])
32
- ' | paste - - - - -
33
- )"
20
+ if command -v jq &>/dev/null; then
21
+ read -r TOOL_NAME SESSION_ID HAS_OUTPUT INPUT_JSON OUTPUT_JSON <<< "$(
22
+ printf '%s' "$INPUT" | jq -r '
23
+ (.tool_name // ""),
24
+ (.session_id // ""),
25
+ (if has("tool_output") then "yes" else "no" end),
26
+ ((.tool_input // {} | tostring) | .[0:500]),
27
+ ((.tool_output // {} | tostring) | .[0:200])
28
+ ' | paste - - - - -
29
+ )"
30
+ else
31
+ # Fallback: extract tool_name with basic pattern matching
32
+ TOOL_NAME="$(printf '%s' "$INPUT" | sed -n 's/.*"tool_name" *: *"\([^"]*\)".*/\1/p' | head -1)"
33
+ SESSION_ID="$(printf '%s' "$INPUT" | sed -n 's/.*"session_id" *: *"\([^"]*\)".*/\1/p' | head -1)"
34
+ HAS_OUTPUT="no"
35
+ printf '%s' "$INPUT" | grep -q '"tool_output"' && HAS_OUTPUT="yes"
36
+ INPUT_JSON="$(printf '%s' "$INPUT" | head -c 500)"
37
+ OUTPUT_JSON=""
38
+ fi
39
+
40
+ [[ -z "$TOOL_NAME" ]] && exit 0
34
41
 
35
42
  # Determine event type
36
43
  if [[ "$HAS_OUTPUT" == "yes" ]]; then
@@ -40,27 +47,24 @@ else
40
47
  fi
41
48
 
42
49
  # ---------------------------------------------------------------------------
43
- # Project detection (4 priority levels)
50
+ # Project detection
44
51
  # ---------------------------------------------------------------------------
45
52
  PROJECT_ROOT=""
46
53
 
47
- # Priority 1: $CLAUDE_PROJECT_DIR env var
48
54
  if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "${CLAUDE_PROJECT_DIR}" ]]; then
49
55
  PROJECT_ROOT="${CLAUDE_PROJECT_DIR}"
50
56
  fi
51
57
 
52
- # Priority 2+3: git repo root (covers both remote-url and root-hash priorities)
53
58
  if [[ -z "$PROJECT_ROOT" ]]; then
54
59
  PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
55
60
  fi
56
61
 
57
- # Priority 4: global fallback
58
62
  if [[ -z "$PROJECT_ROOT" ]]; then
59
63
  PROJECT_ROOT="global"
60
64
  fi
61
65
 
62
66
  # ---------------------------------------------------------------------------
63
- # Compute project hash and name (SHA-256 first 12 chars)
67
+ # Compute project hash and name
64
68
  # ---------------------------------------------------------------------------
65
69
  PROJECT_HASH="$(printf '%s' "$PROJECT_ROOT" | sha256sum | cut -c1-12)"
66
70
  PROJECT_NAME="$(basename "${PROJECT_ROOT%.git}")"
@@ -68,20 +72,18 @@ PROJECT_NAME="$(basename "${PROJECT_ROOT%.git}")"
68
72
  # ---------------------------------------------------------------------------
69
73
  # Directory setup
70
74
  # ---------------------------------------------------------------------------
71
- PROJECT_OBS_DIR="${PROJECTS_DIR}/${PROJECT_HASH}"
72
- OBS_FILE="${PROJECT_OBS_DIR}/observations.jsonl"
75
+ PROJECT_DIR="${INSTINCTS_DIR}/${PROJECT_HASH}"
76
+ OBS_FILE="${PROJECT_DIR}/observations.jsonl"
73
77
 
74
- # Create dirs only if needed (fast no-op if already exists)
75
- [[ -d "$PROJECT_OBS_DIR" ]] || mkdir -p "${PROJECT_OBS_DIR}/observations.archive"
78
+ [[ -d "$PROJECT_DIR" ]] || mkdir -p "$PROJECT_DIR"
76
79
 
77
80
  # ---------------------------------------------------------------------------
78
- # Rotate observations.jsonl if it exceeds 10,000 lines
81
+ # Rotate observations.jsonl at 10,000 lines
79
82
  # ---------------------------------------------------------------------------
80
83
  if [[ -f "$OBS_FILE" ]]; then
81
84
  LINE_COUNT="$(wc -l < "$OBS_FILE")"
82
85
  if (( LINE_COUNT >= 10000 )); then
83
- ARCHIVE_DATE="$(date -u +"%Y-%m-%d")"
84
- mv "$OBS_FILE" "${PROJECT_OBS_DIR}/observations.archive/${ARCHIVE_DATE}.jsonl"
86
+ mv "$OBS_FILE" "${PROJECT_DIR}/observations.$(date -u +"%Y-%m-%d").jsonl"
85
87
  fi
86
88
  fi
87
89
 
@@ -90,44 +92,43 @@ fi
90
92
  # ---------------------------------------------------------------------------
91
93
  TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
92
94
 
93
- printf '%s\n' "$(jq -cn \
94
- --arg ts "$TS" \
95
- --arg event "$EVENT" \
96
- --arg session "$SESSION_ID" \
97
- --arg tool "$TOOL_NAME" \
98
- --arg input_summary "$INPUT_JSON" \
99
- --arg output_summary "$OUTPUT_JSON" \
100
- --arg project_id "$PROJECT_HASH" \
101
- --arg project_name "$PROJECT_NAME" \
102
- '{ts:$ts,event:$event,session:$session,tool:$tool,input_summary:$input_summary,output_summary:$output_summary,project_id:$project_id,project_name:$project_name}')" \
103
- >> "$OBS_FILE"
95
+ if command -v jq &>/dev/null; then
96
+ printf '%s\n' "$(jq -cn \
97
+ --arg ts "$TS" \
98
+ --arg event "$EVENT" \
99
+ --arg session "$SESSION_ID" \
100
+ --arg tool "$TOOL_NAME" \
101
+ --arg input_summary "$INPUT_JSON" \
102
+ --arg output_summary "$OUTPUT_JSON" \
103
+ --arg project_id "$PROJECT_HASH" \
104
+ --arg project_name "$PROJECT_NAME" \
105
+ '{ts:$ts,event:$event,session:$session,tool:$tool,input_summary:$input_summary,output_summary:$output_summary,project_id:$project_id,project_name:$project_name}')" \
106
+ >> "$OBS_FILE"
107
+ else
108
+ # Fallback: manual JSON construction
109
+ printf '{"ts":"%s","event":"%s","session":"%s","tool":"%s","project_id":"%s","project_name":"%s"}\n' \
110
+ "$TS" "$EVENT" "$SESSION_ID" "$TOOL_NAME" "$PROJECT_HASH" "$PROJECT_NAME" \
111
+ >> "$OBS_FILE"
112
+ fi
104
113
 
105
114
  # ---------------------------------------------------------------------------
106
- # Write project.json and update registry (only if new — deferred to avoid
107
- # adding latency to every invocation)
115
+ # Write project.json if new project
108
116
  # ---------------------------------------------------------------------------
109
- PROJECT_JSON="${PROJECT_OBS_DIR}/project.json"
117
+ PROJECT_JSON="${PROJECT_DIR}/project.json"
110
118
  if [[ ! -f "$PROJECT_JSON" ]]; then
111
- CREATED_AT="$TS"
112
- jq -n \
113
- --arg id "$PROJECT_HASH" \
114
- --arg name "$PROJECT_NAME" \
115
- --arg root "$PROJECT_ROOT" \
116
- --arg created_at "$CREATED_AT" \
117
- '{id:$id,name:$name,root:$root,created_at:$created_at}' \
118
- > "$PROJECT_JSON"
119
-
120
- # Update global projects.json registry
121
- mkdir -p "$MULAHAZAH_DIR"
122
- [[ -f "$GLOBAL_REGISTRY" ]] || printf '{}' > "$GLOBAL_REGISTRY"
123
-
124
- TMP_REGISTRY="$(mktemp)"
125
- jq --arg id "$PROJECT_HASH" \
126
- --arg name "$PROJECT_NAME" \
127
- --arg root "$PROJECT_ROOT" \
128
- --arg created_at "$CREATED_AT" \
129
- '.[$id] = {name:$name,root:$root,created_at:$created_at}' \
130
- "$GLOBAL_REGISTRY" > "$TMP_REGISTRY" && mv "$TMP_REGISTRY" "$GLOBAL_REGISTRY"
119
+ if command -v jq &>/dev/null; then
120
+ jq -n \
121
+ --arg id "$PROJECT_HASH" \
122
+ --arg name "$PROJECT_NAME" \
123
+ --arg root "$PROJECT_ROOT" \
124
+ --arg created_at "$TS" \
125
+ '{id:$id,name:$name,root:$root,created_at:$created_at}' \
126
+ > "$PROJECT_JSON"
127
+ else
128
+ printf '{"id":"%s","name":"%s","root":"%s","created_at":"%s"}\n' \
129
+ "$PROJECT_HASH" "$PROJECT_NAME" "$PROJECT_ROOT" "$TS" \
130
+ > "$PROJECT_JSON"
131
+ fi
131
132
  fi
132
133
 
133
134
  exit 0