create-merlin-brain 2.3.3 → 2.4.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.
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Merlin Loop - Mode Management
4
+ # Handles automated, interactive, and hybrid loop modes
5
+ #
6
+
7
+ # Load discussion mode if available
8
+ if [ -f "$SCRIPT_DIR/lib/discuss.sh" ]; then
9
+ source "$SCRIPT_DIR/lib/discuss.sh"
10
+ fi
11
+
12
+ # Load timeout if available
13
+ if [ -f "$SCRIPT_DIR/lib/timeout.sh" ]; then
14
+ source "$SCRIPT_DIR/lib/timeout.sh"
15
+ fi
16
+
17
+ # ═══════════════════════════════════════════════════════════════════════════════
18
+ # Mode Constants
19
+ # ═══════════════════════════════════════════════════════════════════════════════
20
+
21
+ MODE_AUTO="auto"
22
+ MODE_INTERACTIVE="interactive"
23
+ MODE_HYBRID="hybrid"
24
+
25
+ # Default mode
26
+ LOOP_MODE="${LOOP_MODE:-$MODE_HYBRID}"
27
+
28
+ # Hybrid mode settings
29
+ PAUSE_INTERVAL="${MERLIN_PAUSE_INTERVAL:-5}" # Pause every N tasks in hybrid mode
30
+
31
+ # Pause reason tracking
32
+ LAST_PAUSE_REASON=""
33
+
34
+ # ═══════════════════════════════════════════════════════════════════════════════
35
+ # Pause Detection
36
+ # ═══════════════════════════════════════════════════════════════════════════════
37
+
38
+ # Check if we should pause based on mode and signals
39
+ # Args: $1 = output from Claude, $2 = current task count
40
+ # Returns: 0 if should pause, 1 if should continue
41
+ should_pause() {
42
+ local output="$1"
43
+ local task_count="${2:-0}"
44
+
45
+ case "$LOOP_MODE" in
46
+ "$MODE_AUTO")
47
+ # Auto mode: only pause on explicit CHECKPOINT_SIGNAL
48
+ if echo "$output" | grep -q "CHECKPOINT_SIGNAL"; then
49
+ LAST_PAUSE_REASON="checkpoint"
50
+ return 0
51
+ fi
52
+ return 1
53
+ ;;
54
+
55
+ "$MODE_INTERACTIVE")
56
+ # Interactive mode: always pause after every task
57
+ LAST_PAUSE_REASON="interactive"
58
+ return 0
59
+ ;;
60
+
61
+ "$MODE_HYBRID")
62
+ # Hybrid mode: pause on decisions, low confidence, checkpoints, or every N tasks
63
+
64
+ # Check for decision needed
65
+ if echo "$output" | grep -q "DECISION_NEEDED"; then
66
+ LAST_PAUSE_REASON="decision"
67
+ return 0
68
+ fi
69
+
70
+ # Check for low confidence
71
+ if echo "$output" | grep -q "LOW_CONFIDENCE"; then
72
+ LAST_PAUSE_REASON="low_confidence"
73
+ return 0
74
+ fi
75
+
76
+ # Check for explicit checkpoint
77
+ if echo "$output" | grep -q "CHECKPOINT_SIGNAL"; then
78
+ LAST_PAUSE_REASON="checkpoint"
79
+ return 0
80
+ fi
81
+
82
+ # Check for periodic pause (every N tasks)
83
+ if [ "$PAUSE_INTERVAL" -gt 0 ] && [ "$task_count" -gt 0 ]; then
84
+ if [ $((task_count % PAUSE_INTERVAL)) -eq 0 ]; then
85
+ LAST_PAUSE_REASON="periodic"
86
+ return 0
87
+ fi
88
+ fi
89
+
90
+ return 1
91
+ ;;
92
+
93
+ *)
94
+ # Unknown mode, default to continue
95
+ return 1
96
+ ;;
97
+ esac
98
+ }
99
+
100
+ # Get human-readable pause reason
101
+ get_pause_reason() {
102
+ case "$LAST_PAUSE_REASON" in
103
+ "checkpoint")
104
+ echo "Explicit checkpoint requested"
105
+ ;;
106
+ "decision")
107
+ echo "Decision needed - Claude wants your input"
108
+ ;;
109
+ "low_confidence")
110
+ echo "Low confidence - Claude is uncertain about approach"
111
+ ;;
112
+ "interactive")
113
+ echo "Interactive mode - pausing after every task"
114
+ ;;
115
+ "periodic")
116
+ echo "Periodic checkpoint (every $PAUSE_INTERVAL tasks)"
117
+ ;;
118
+ *)
119
+ echo "Pause requested"
120
+ ;;
121
+ esac
122
+ }
123
+
124
+ # Extract signal details from output
125
+ # Format: SIGNAL_NAME|reason|details
126
+ extract_signal_details() {
127
+ local output="$1"
128
+ local signal_type="$2"
129
+
130
+ local signal_line
131
+ signal_line=$(echo "$output" | grep -o "${signal_type}|[^|]*|[^|]*" | head -1)
132
+
133
+ if [ -n "$signal_line" ]; then
134
+ local reason
135
+ reason=$(echo "$signal_line" | cut -d'|' -f2)
136
+ local details
137
+ details=$(echo "$signal_line" | cut -d'|' -f3)
138
+ echo "Reason: $reason"
139
+ [ -n "$details" ] && echo "Details: $details"
140
+ fi
141
+ }
142
+
143
+ # ═══════════════════════════════════════════════════════════════════════════════
144
+ # Pause Handler
145
+ # ═══════════════════════════════════════════════════════════════════════════════
146
+
147
+ # Handle a pause point with user interaction
148
+ # Returns: 0 = continue, 1 = quit, 2 = skip task
149
+ handle_pause() {
150
+ local output="$1"
151
+ local iteration="$2"
152
+ local task_count="$3"
153
+
154
+ echo ""
155
+ echo -e "${MAGENTA}╔═══════════════════════════════════════════════════════════════╗${RESET}"
156
+ echo -e "${MAGENTA}║ ${BOLD}PAUSE POINT${RESET}${MAGENTA} ║${RESET}"
157
+ echo -e "${MAGENTA}╚═══════════════════════════════════════════════════════════════╝${RESET}"
158
+ echo ""
159
+
160
+ # Show pause reason
161
+ local reason
162
+ reason=$(get_pause_reason)
163
+ echo -e "${YELLOW}Why paused:${RESET} $reason"
164
+ echo ""
165
+
166
+ # Show signal details if present
167
+ if [ "$LAST_PAUSE_REASON" = "decision" ]; then
168
+ echo -e "${CYAN}Decision details:${RESET}"
169
+ extract_signal_details "$output" "DECISION_NEEDED"
170
+ echo ""
171
+ elif [ "$LAST_PAUSE_REASON" = "low_confidence" ]; then
172
+ echo -e "${CYAN}Uncertainty details:${RESET}"
173
+ extract_signal_details "$output" "LOW_CONFIDENCE"
174
+ echo ""
175
+ fi
176
+
177
+ # Show progress
178
+ echo -e "${BLUE}Progress:${RESET} Iteration $iteration, Tasks completed: $task_count"
179
+ echo ""
180
+
181
+ # Show menu
182
+ echo -e "${BOLD}Options:${RESET}"
183
+ echo -e " ${GREEN}[c]${RESET} Continue to next task"
184
+ echo -e " ${CYAN}[d]${RESET} Discuss (spawn fresh Claude for conversation)"
185
+ echo -e " ${YELLOW}[s]${RESET} Skip current task"
186
+ echo -e " ${RED}[q]${RESET} Quit and save state"
187
+ echo ""
188
+
189
+ # Use timeout if available, otherwise standard read
190
+ local response
191
+ if declare -f read_with_timeout &> /dev/null; then
192
+ response=$(read_with_timeout "Your choice [c/d/s/q]: " "c")
193
+ else
194
+ echo -n "Your choice [c/d/s/q]: "
195
+ read -r response
196
+ response="${response:-c}"
197
+ fi
198
+
199
+ case "${response,,}" in
200
+ c|continue)
201
+ echo -e "${GREEN}Continuing...${RESET}"
202
+ return 0
203
+ ;;
204
+ d|discuss)
205
+ echo -e "${CYAN}Launching discussion mode...${RESET}"
206
+ # Check if discuss.sh is available
207
+ if declare -f spawn_discussion &> /dev/null; then
208
+ spawn_discussion "$output" "$iteration"
209
+ # After discussion, show menu again
210
+ handle_pause "$output" "$iteration" "$task_count"
211
+ return $?
212
+ else
213
+ echo -e "${YELLOW}Discussion mode not available. Continuing...${RESET}"
214
+ return 0
215
+ fi
216
+ ;;
217
+ s|skip)
218
+ echo -e "${YELLOW}Skipping current task...${RESET}"
219
+ record_task_skipped "$iteration"
220
+ return 2
221
+ ;;
222
+ q|quit)
223
+ echo -e "${RED}Saving state and exiting...${RESET}"
224
+ update_state "status" "paused"
225
+ return 1
226
+ ;;
227
+ *)
228
+ echo -e "${YELLOW}Unknown option. Continuing...${RESET}"
229
+ return 0
230
+ ;;
231
+ esac
232
+ }
233
+
234
+ # Record that a task was skipped
235
+ record_task_skipped() {
236
+ local iteration="$1"
237
+ local now
238
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
239
+
240
+ if [ -n "$HISTORY_FILE" ]; then
241
+ echo "{\"type\":\"task_skipped\",\"iteration\":$iteration,\"timestamp\":\"$now\"}" >> "$HISTORY_FILE"
242
+ fi
243
+ }
244
+
245
+ # ═══════════════════════════════════════════════════════════════════════════════
246
+ # Mode Display
247
+ # ═══════════════════════════════════════════════════════════════════════════════
248
+
249
+ # Get mode display name with description
250
+ get_mode_display() {
251
+ case "$LOOP_MODE" in
252
+ "$MODE_AUTO")
253
+ echo "Automated (pauses only on explicit checkpoints)"
254
+ ;;
255
+ "$MODE_INTERACTIVE")
256
+ echo "Interactive (pauses after every task)"
257
+ ;;
258
+ "$MODE_HYBRID")
259
+ echo "Hybrid (pauses on decisions, low confidence, every ${PAUSE_INTERVAL} tasks)"
260
+ ;;
261
+ *)
262
+ echo "$LOOP_MODE"
263
+ ;;
264
+ esac
265
+ }
266
+
267
+ # Validate mode value
268
+ validate_mode() {
269
+ local mode="$1"
270
+
271
+ case "$mode" in
272
+ "$MODE_AUTO"|"$MODE_INTERACTIVE"|"$MODE_HYBRID")
273
+ return 0
274
+ ;;
275
+ *)
276
+ echo -e "${RED}Invalid mode: $mode${RESET}"
277
+ echo "Valid modes: auto, interactive, hybrid"
278
+ return 1
279
+ ;;
280
+ esac
281
+ }
282
+
283
+ # Set loop mode with validation
284
+ set_loop_mode() {
285
+ local mode="$1"
286
+
287
+ if validate_mode "$mode"; then
288
+ LOOP_MODE="$mode"
289
+ echo -e "${GREEN}Loop mode set to: $(get_mode_display)${RESET}"
290
+ return 0
291
+ fi
292
+
293
+ return 1
294
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Merlin Loop - Session End Protocol
4
+ # Enforces commit + sync + checkpoint at every task end
5
+ # Part of Merlin Pro v1.0
6
+ #
7
+
8
+ # ═══════════════════════════════════════════════════════════════════════════════
9
+ # Configuration
10
+ # ═══════════════════════════════════════════════════════════════════════════════
11
+
12
+ SESSION_END_AUTO_COMMIT="${MERLIN_AUTO_COMMIT:-true}"
13
+ SESSION_END_AUTO_PUSH="${MERLIN_AUTO_PUSH:-true}"
14
+ SESSION_END_AUTO_CHECKPOINT="${MERLIN_AUTO_CHECKPOINT:-true}"
15
+ SESSION_END_REQUIRE_CLEAN="${MERLIN_REQUIRE_CLEAN:-false}"
16
+
17
+ # ═══════════════════════════════════════════════════════════════════════════════
18
+ # Session End Protocol
19
+ # ═══════════════════════════════════════════════════════════════════════════════
20
+
21
+ # Main session end protocol - call this after every task
22
+ session_end_protocol() {
23
+ local task_description="${1:-Task completed}"
24
+ local task_id="${2:-}"
25
+ local files_changed="${3:-}" # Comma-separated list or empty for auto-detect
26
+
27
+ echo ""
28
+ echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
29
+ echo -e "${BOLD}📦 SESSION END PROTOCOL${RESET}"
30
+ echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
31
+ echo ""
32
+
33
+ local success=true
34
+ local commit_hash=""
35
+
36
+ # Step 1: Check for uncommitted changes
37
+ echo -e "${CYAN}[1/5]${RESET} Checking for changes..."
38
+ local has_changes
39
+ has_changes=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
40
+
41
+ if [ "$has_changes" -eq 0 ]; then
42
+ echo -e " ${GREEN}✓${RESET} No uncommitted changes"
43
+ else
44
+ echo -e " ${YELLOW}!${RESET} Found ${has_changes} changed files"
45
+
46
+ # Step 2: Auto-commit if enabled
47
+ if [ "$SESSION_END_AUTO_COMMIT" == "true" ]; then
48
+ echo -e "${CYAN}[2/5]${RESET} Committing changes..."
49
+
50
+ # Stage all changes (or specific files if provided)
51
+ if [ -n "$files_changed" ]; then
52
+ # Stage specific files
53
+ IFS=',' read -ra FILES <<< "$files_changed"
54
+ for file in "${FILES[@]}"; do
55
+ git add "$file" 2>/dev/null || true
56
+ done
57
+ else
58
+ # Stage all changes
59
+ git add -A
60
+ fi
61
+
62
+ # Create commit message
63
+ local commit_msg="feat: ${task_description}"
64
+ if [ -n "$task_id" ]; then
65
+ commit_msg="${commit_msg}
66
+
67
+ Task: ${task_id}
68
+ Co-Authored-By: Merlin Loop <noreply@merlin.build>"
69
+ fi
70
+
71
+ # Commit
72
+ if git commit -m "$commit_msg" 2>/dev/null; then
73
+ commit_hash=$(git rev-parse HEAD 2>/dev/null)
74
+ echo -e " ${GREEN}✓${RESET} Committed: ${commit_hash:0:7}"
75
+ else
76
+ echo -e " ${YELLOW}⚠${RESET} Commit failed (pre-commit hook or no changes)"
77
+ success=false
78
+ fi
79
+ else
80
+ echo -e "${CYAN}[2/5]${RESET} Auto-commit disabled, skipping"
81
+ fi
82
+ fi
83
+
84
+ # Step 3: Push to remote
85
+ if [ "$SESSION_END_AUTO_PUSH" == "true" ] && [ -n "$commit_hash" ]; then
86
+ echo -e "${CYAN}[3/5]${RESET} Pushing to remote..."
87
+
88
+ if git push 2>/dev/null; then
89
+ echo -e " ${GREEN}✓${RESET} Pushed to remote"
90
+ else
91
+ echo -e " ${YELLOW}⚠${RESET} Push failed (network or permissions)"
92
+ fi
93
+ else
94
+ echo -e "${CYAN}[3/5]${RESET} Push skipped"
95
+ fi
96
+
97
+ # Step 4: Record commit to Sights activity tracking
98
+ echo -e "${CYAN}[4/5]${RESET} Recording to Sights..."
99
+ if [ -n "$commit_hash" ] && type record_last_commit &> /dev/null; then
100
+ if record_last_commit "$task_id" "merlin-loop" "loop-agent"; then
101
+ echo -e " ${GREEN}✓${RESET} Recorded in Sights activity"
102
+ else
103
+ echo -e " ${YELLOW}⚠${RESET} Sights recording failed (API key or connection)"
104
+ fi
105
+ else
106
+ echo -e " ${DIM}Skipped (no commit or Sights not available)${RESET}"
107
+ fi
108
+
109
+ # Step 5: Save checkpoint
110
+ if [ "$SESSION_END_AUTO_CHECKPOINT" == "true" ]; then
111
+ echo -e "${CYAN}[5/5]${RESET} Saving checkpoint..."
112
+
113
+ if type save_checkpoint &> /dev/null; then
114
+ # Get current task info
115
+ local current_phase=""
116
+ local current_task=""
117
+
118
+ if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
119
+ current_phase=$(jq -r '.current_phase // empty' "$STATE_FILE" 2>/dev/null || echo "")
120
+ current_task=$(jq -r '.current_task // empty' "$STATE_FILE" 2>/dev/null || echo "")
121
+ fi
122
+
123
+ # Get files as JSON array
124
+ local files_json="[]"
125
+ if [ -n "$files_changed" ]; then
126
+ files_json=$(echo "$files_changed" | jq -R 'split(",")' 2>/dev/null || echo "[]")
127
+ elif [ -n "$commit_hash" ]; then
128
+ files_json=$(git diff-tree --no-commit-id --name-only -r "$commit_hash" 2>/dev/null | jq -R -s 'split("\n") | map(select(. != ""))' || echo "[]")
129
+ fi
130
+
131
+ if save_checkpoint "$task_description" "$current_phase" "$current_task" "$files_json" "merlin-loop"; then
132
+ echo -e " ${GREEN}✓${RESET} Checkpoint saved to cloud"
133
+ else
134
+ echo -e " ${YELLOW}⚠${RESET} Checkpoint save failed"
135
+ fi
136
+ else
137
+ echo -e " ${DIM}Skipped (checkpoint function not available)${RESET}"
138
+ fi
139
+ else
140
+ echo -e "${CYAN}[5/5]${RESET} Auto-checkpoint disabled, skipping"
141
+ fi
142
+
143
+ # Step 6: Release task claim (if held)
144
+ if [ -n "$task_id" ] && type release_task &> /dev/null; then
145
+ release_task "$task_id" 2>/dev/null || true
146
+ fi
147
+
148
+ echo ""
149
+ echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
150
+
151
+ if [ "$success" == "true" ]; then
152
+ echo -e "${GREEN}✅ Session end protocol complete${RESET}"
153
+ else
154
+ echo -e "${YELLOW}⚠️ Session end protocol completed with warnings${RESET}"
155
+ fi
156
+
157
+ echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
158
+ echo ""
159
+
160
+ return 0
161
+ }
162
+
163
+ # ═══════════════════════════════════════════════════════════════════════════════
164
+ # Quick Protocol (minimal version)
165
+ # ═══════════════════════════════════════════════════════════════════════════════
166
+
167
+ # Quick session end - just commit and checkpoint, no visual output
168
+ session_end_quick() {
169
+ local task_description="${1:-Task completed}"
170
+ local task_id="${2:-}"
171
+
172
+ # Check for changes
173
+ local has_changes
174
+ has_changes=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
175
+
176
+ if [ "$has_changes" -gt 0 ] && [ "$SESSION_END_AUTO_COMMIT" == "true" ]; then
177
+ git add -A
178
+ git commit -m "feat: ${task_description}" --quiet 2>/dev/null || true
179
+ git push --quiet 2>/dev/null || true
180
+ record_last_commit "$task_id" "merlin-loop" "loop-agent" 2>/dev/null || true
181
+ fi
182
+
183
+ if [ "$SESSION_END_AUTO_CHECKPOINT" == "true" ] && type save_checkpoint &> /dev/null; then
184
+ save_checkpoint "$task_description" "" "" "[]" "merlin-loop" 2>/dev/null || true
185
+ fi
186
+ }
187
+
188
+ # ═══════════════════════════════════════════════════════════════════════════════
189
+ # Validation
190
+ # ═══════════════════════════════════════════════════════════════════════════════
191
+
192
+ # Check if work tree is clean (for strict mode)
193
+ check_clean_worktree() {
194
+ local has_changes
195
+ has_changes=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
196
+
197
+ if [ "$has_changes" -eq 0 ]; then
198
+ return 0
199
+ else
200
+ if [ "$SESSION_END_REQUIRE_CLEAN" == "true" ]; then
201
+ echo -e "${RED}Error: Work tree not clean. Commit or stash changes before continuing.${RESET}"
202
+ return 1
203
+ fi
204
+ return 0
205
+ fi
206
+ }
207
+
208
+ # Verify session end was successful
209
+ verify_session_end() {
210
+ local errors=0
211
+
212
+ # Check if work tree is clean (if required)
213
+ if [ "$SESSION_END_REQUIRE_CLEAN" == "true" ]; then
214
+ local has_changes
215
+ has_changes=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
216
+ if [ "$has_changes" -gt 0 ]; then
217
+ echo -e "${YELLOW}Warning: Uncommitted changes remain${RESET}"
218
+ ((errors++))
219
+ fi
220
+ fi
221
+
222
+ # Check if checkpoint was saved (if enabled)
223
+ if [ "$SESSION_END_AUTO_CHECKPOINT" == "true" ] && type fetch_checkpoint &> /dev/null; then
224
+ local checkpoint
225
+ checkpoint=$(fetch_checkpoint 2>/dev/null || echo "")
226
+ if [ -z "$checkpoint" ]; then
227
+ echo -e "${YELLOW}Warning: No checkpoint found${RESET}"
228
+ ((errors++))
229
+ fi
230
+ fi
231
+
232
+ return $errors
233
+ }
234
+
235
+ # ═══════════════════════════════════════════════════════════════════════════════
236
+ # Integration Hook
237
+ # ═══════════════════════════════════════════════════════════════════════════════
238
+
239
+ # Hook to call after Claude completes a task
240
+ # Usage: after_task_hook "task description" "task-id" "file1.ts,file2.ts"
241
+ after_task_hook() {
242
+ session_end_protocol "$@"
243
+ }
244
+
245
+ # Export for use by other scripts
246
+ export -f session_end_protocol 2>/dev/null || true
247
+ export -f session_end_quick 2>/dev/null || true
248
+ export -f after_task_hook 2>/dev/null || true