monomind 1.10.41 → 1.10.42

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,1275 @@
1
+ ---
2
+ name: mastermind-monitor
3
+ description: Mastermind monitor — a forever-running task executor that watches Linear, GitHub Issues/PRs, Monotask boards, and filesystem folders for new tasks, claims them, executes them with the right agent, posts progress comments, and advances status at every stage. Supports per-user/per-state filtering, 3-retry failure handling, and a single concurrent task at a time (safe default). Persists state across cycles via ScheduleWakeup.
4
+ type: domain-skill
5
+ default_mode: confirm
6
+ ---
7
+
8
+ # Mastermind Monitor
9
+
10
+ Invoked via `mastermind:monitor` or `/mastermind:monitor`.
11
+
12
+ A monitor is a named, forever-running task executor. It polls one or more task sources on a configurable interval, claims matching tasks, hands them off to a Claude agent for execution, posts back results as comments/status updates, and self-reschedules via `ScheduleWakeup`.
13
+
14
+ ---
15
+
16
+ ## CLI Flags
17
+
18
+ ```
19
+ --action start | stop | pause | resume | status | list | add-source | tick
20
+ --name monitor name (slug, e.g. "dev-agent")
21
+ --source linear | github | monotask | filesystem
22
+ --interval poll interval in seconds (default: 120)
23
+ --user filter by assignee username/email (can be repeated)
24
+ --state filter by task state/status (can be repeated, default: open/todo)
25
+ --max-concurrent max tasks in flight (default: 1)
26
+ --agent agent type to execute tasks (default: coder)
27
+ --project monotask project/board name, or gh repo (org/repo)
28
+ --team Linear team ID or slug
29
+ --folder filesystem folder path to watch (for source=filesystem)
30
+ --label filter by label (can be repeated)
31
+ --caller command | master (internal — skip brain load if "command")
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Step 0 — Brain Load (standalone only)
37
+
38
+ If `caller` is not "command", load brain context following `_protocol.md` Brain Load Procedure with namespace: `ops`.
39
+
40
+ ---
41
+
42
+ ## Step 1 — Resolve Monitor Config Directory
43
+
44
+ ```bash
45
+ MONITOR_DIR=".monomind/monitor"
46
+ mkdir -p "$MONITOR_DIR"
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Step 2 — Dispatch by Action
52
+
53
+ ### `list` (default when no --action)
54
+
55
+ ```bash
56
+ echo "MONITORS"
57
+ echo "────────────────────────────────────────"
58
+ for f in "$MONITOR_DIR"/*.json; do
59
+ [ -f "$f" ] || continue
60
+ jq -r '
61
+ "[\(.name)] status=\(.status // "active") interval=\(.poll_interval)s agent=\(.agent_type)
62
+ sources: \([.sources[].type] | join(", "))
63
+ last_tick: \(.last_tick // "never") tasks_done: \(.stats.done // 0) tasks_failed: \(.stats.failed // 0)"
64
+ ' "$f"
65
+ echo ""
66
+ done
67
+ ```
68
+
69
+ ---
70
+
71
+ ### `start`
72
+
73
+ Creates a new monitor config and triggers the first tick.
74
+
75
+ **Required:** `--name`
76
+
77
+ ```bash
78
+ cfg="$MONITOR_DIR/${name}.json"
79
+ if [ -f "$cfg" ]; then
80
+ echo "Monitor '$name' already exists. Use --action resume to restart it."
81
+ exit 0
82
+ fi
83
+
84
+ cat > "$cfg" <<EOF
85
+ {
86
+ "name": "${name}",
87
+ "status": "active",
88
+ "poll_interval": ${interval:-120},
89
+ "agent_type": "${agent:-coder}",
90
+ "max_concurrent": ${max_concurrent:-1},
91
+ "sources": [],
92
+ "users": [],
93
+ "states": [],
94
+ "labels": [],
95
+ "stats": { "done": 0, "failed": 0, "retried": 0, "total_claimed": 0 },
96
+ "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
97
+ "last_tick": null
98
+ }
99
+ EOF
100
+
101
+ echo "Monitor '$name' created."
102
+ echo "Add sources with: /mastermind:monitor --action add-source --name $name --source linear ..."
103
+ echo "Then start the tick loop."
104
+ ```
105
+
106
+ After creating the config, if at least one source was provided (via `--source` + related flags), also run the `add-source` logic for each provided source before the loop starts.
107
+
108
+ Then **immediately call `ScheduleWakeup`** with:
109
+ - `delaySeconds`: 10 (first tick almost immediately)
110
+ - `prompt`: `/mastermind:monitor --action tick --name <name>`
111
+ - `reason`: `First tick for monitor: <name>`
112
+
113
+ ---
114
+
115
+ ### `stop`
116
+
117
+ ```bash
118
+ cfg="$MONITOR_DIR/${name}.json"
119
+ [ ! -f "$cfg" ] && echo "Monitor '$name' not found." && exit 1
120
+ tmp="${cfg}.tmp"
121
+ jq '.status = "stopped"' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
122
+ echo "Monitor '$name' stopped. It will not reschedule after the current tick finishes."
123
+ ```
124
+
125
+ ---
126
+
127
+ ### `pause` / `resume`
128
+
129
+ ```bash
130
+ cfg="$MONITOR_DIR/${name}.json"
131
+ [ ! -f "$cfg" ] && echo "Monitor '$name' not found." && exit 1
132
+ tmp="${cfg}.tmp"
133
+ new_status=$([ "$action" = "pause" ] && echo "paused" || echo "active")
134
+ jq --arg s "$new_status" '.status = $s' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
135
+ echo "Monitor '$name' is now: $new_status"
136
+ [ "$action" = "resume" ] && echo "Scheduling next tick..." # then call ScheduleWakeup
137
+ ```
138
+
139
+ If `action=resume`, call `ScheduleWakeup`:
140
+ - `delaySeconds`: 10
141
+ - `prompt`: `/mastermind:monitor --action tick --name <name>`
142
+ - `reason`: `Resuming monitor: <name>`
143
+
144
+ ---
145
+
146
+ ### `status`
147
+
148
+ ```bash
149
+ cfg="$MONITOR_DIR/${name}.json"
150
+ state_file="$MONITOR_DIR/${name}-state.json"
151
+ [ ! -f "$cfg" ] && echo "Monitor '$name' not found." && exit 1
152
+
153
+ jq -r '
154
+ "MONITOR: \(.name)",
155
+ "Status: \(.status)",
156
+ "Agent: \(.agent_type) | Interval: \(.poll_interval)s | Max concurrent: \(.max_concurrent)",
157
+ "Last tick: \(.last_tick // "never")",
158
+ "",
159
+ "Stats:",
160
+ " Claimed: \(.stats.total_claimed) Done: \(.stats.done) Failed: \(.stats.failed) Retried: \(.stats.retried)",
161
+ "",
162
+ "Sources (\(.sources | length)):"
163
+ ' "$cfg"
164
+
165
+ jq -r '.sources[] | " [\(.type)] \(.filter | to_entries | map("\(.key)=\(.value)") | join(" "))"' "$cfg"
166
+
167
+ if [ -f "$state_file" ]; then
168
+ in_flight=$(jq '.in_flight // [] | length' "$state_file" 2>/dev/null || echo 0)
169
+ echo ""
170
+ echo "In-flight tasks: $in_flight"
171
+ jq -r '.in_flight[]? |
172
+ " [\(.source_type):\(.external_id)] claimed_at=\(.claimed_at) retry=\(.retry_count) last_failure=\(.last_failure // "n/a")"' \
173
+ "$state_file" 2>/dev/null
174
+ fi
175
+ ```
176
+
177
+ ---
178
+
179
+ ### `add-source`
180
+
181
+ Appends a new source adapter config to an existing monitor.
182
+
183
+ **Required:** `--name`, `--source`
184
+
185
+ ```bash
186
+ cfg="$MONITOR_DIR/${name}.json"
187
+ [ ! -f "$cfg" ] && echo "Monitor '$name' not found. Run --action start first." && exit 1
188
+ ```
189
+
190
+ Build the source object based on `--source`:
191
+
192
+ **linear:**
193
+ ```json
194
+ {
195
+ "type": "linear",
196
+ "filter": {
197
+ "team": "<--team value>",
198
+ "assignees": ["<--user values>"],
199
+ "states": ["<--state values, default: Todo>"],
200
+ "labels": ["<--label values>"],
201
+ "project": "<--project value if given>"
202
+ }
203
+ }
204
+ ```
205
+
206
+ **github:**
207
+ ```json
208
+ {
209
+ "type": "github",
210
+ "filter": {
211
+ "repo": "<--project value, e.g. org/repo>",
212
+ "assignee": "<--user value>",
213
+ "labels": ["<--label values>"],
214
+ "state": "<--state value, default: open>",
215
+ "type": "issue"
216
+ }
217
+ }
218
+ ```
219
+
220
+ **monotask:**
221
+ ```json
222
+ {
223
+ "type": "monotask",
224
+ "filter": {
225
+ "board": "<--project value>",
226
+ "column": "<--state value, default: Todo>",
227
+ "label": "<--label value, default: role:ai-agent>"
228
+ }
229
+ }
230
+ ```
231
+
232
+ **filesystem:**
233
+ ```json
234
+ {
235
+ "type": "filesystem",
236
+ "filter": {
237
+ "folder": "<--folder value>",
238
+ "glob": "*.task",
239
+ "user": "<--user value>"
240
+ }
241
+ }
242
+ ```
243
+
244
+ **Repeated-flag parsing rule:** When `--user`, `--state`, or `--label` is specified multiple times, collect the values into space-separated shell variables `$users`, `$states`, `$labels` (e.g. `users="alice bob"` from `--user alice --user bob`). Single-occurrence flags (`--team`, `--project`, `--folder`) map to `$team`, `$project`, `$folder` directly.
245
+
246
+ > **Linear multi-assignee:** The Linear MCP tool (`mcp__claude_ai_Linear__list_issues`) does not support multiple assignees in a single query. When `$users` contains more than one value, loop over each user and merge results client-side before dedup filtering. For simplicity in v1, only `filter.assignees[0]` is sent per query cycle — document this limitation to users.
247
+
248
+ > **Before calling any `mcp__claude_ai_Linear__*` tool**, confirm availability with `ToolSearch` (`select:mcp__claude_ai_Linear__list_issues,mcp__claude_ai_Linear__save_issue,mcp__claude_ai_Linear__save_comment`) and load the schema. If the Linear MCP server is not registered, skip the Linear source for this tick with a warning.
249
+
250
+ Build and append the source object using `jq -n` — construct the JSON from flags, then append:
251
+
252
+ ```bash
253
+ # Build src_json based on --source type:
254
+ # Repeated flags collected as space-separated: $users, $states, $labels
255
+ # Derive singular $user / $state from first value (adapters that take one value)
256
+ user="${users%% *}"
257
+ state="${states%% *}"
258
+
259
+ case "$source" in
260
+ linear)
261
+ assignees_json=$([ -n "$users" ] && printf '%s\n' $users | jq -R . | jq -sc '.' || echo '[]')
262
+ states_json=$([ -n "$states" ] && printf '%s\n' $states | jq -R . | jq -sc '.' || echo '["Todo"]')
263
+ labels_json=$([ -n "$labels" ] && printf '%s\n' $labels | jq -R . | jq -sc '.' || echo '[]')
264
+ src_json=$(jq -cn \
265
+ --arg team "${team:-}" \
266
+ --arg project "${project:-}" \
267
+ --argjson assignees "$assignees_json" \
268
+ --argjson states "$states_json" \
269
+ --argjson labels "$labels_json" \
270
+ '{"type":"linear","filter":{"team":$team,"assignees":$assignees,"states":$states,"labels":$labels,"project":$project}}')
271
+ ;;
272
+ github)
273
+ labels_json=$([ -n "$labels" ] && printf '%s\n' $labels | jq -R . | jq -sc '.' || echo '[]')
274
+ src_json=$(jq -cn \
275
+ --arg repo "${project:-}" \
276
+ --arg assignee "${user:-}" \
277
+ --argjson labels "$labels_json" \
278
+ --arg state "${state:-open}" \
279
+ '{"type":"github","filter":{"repo":$repo,"assignee":$assignee,"labels":$labels,"state":$state,"type":"issue"}}')
280
+ ;;
281
+ monotask)
282
+ src_json=$(jq -cn \
283
+ --arg board "${project:-}" \
284
+ --arg column "${state:-Todo}" \
285
+ --arg label "${label:-role:ai-agent}" \
286
+ '{"type":"monotask","filter":{"board":$board,"column":$column,"label":$label}}')
287
+ ;;
288
+ filesystem)
289
+ src_json=$(jq -cn \
290
+ --arg folder "${folder:-./tasks}" \
291
+ --arg user "${user:-}" \
292
+ '{"type":"filesystem","filter":{"folder":$folder,"glob":"*.task","user":$user}}')
293
+ ;;
294
+ *)
295
+ echo "Unknown source type: $source. Supported: linear | github | monotask | filesystem"
296
+ exit 1
297
+ ;;
298
+ esac
299
+
300
+ tmp="${cfg}.tmp"
301
+ jq --argjson src "$src_json" '.sources += [$src]' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
302
+ echo "Source added to monitor '$name'."
303
+ ```
304
+
305
+ ---
306
+
307
+ ### `tick` — The Main Execution Loop
308
+
309
+ This is the heart of the monitor. Called by `ScheduleWakeup` every `poll_interval` seconds.
310
+
311
+ > **Execution model:** bash code blocks in this section are executable fragments. Prose instructions between blocks complete the control flow (ScheduleWakeup calls, conditional branches, `fi` closures). Follow both the code and the prose — neither is complete without the other.
312
+
313
+ **Required:** `--name`
314
+
315
+ ```bash
316
+ cfg="$MONITOR_DIR/${name}.json"
317
+ state_file="$MONITOR_DIR/${name}-state.json"
318
+
319
+ # Load config
320
+ [ ! -f "$cfg" ] && echo "Monitor '$name' not found — loop terminated." && exit 0
321
+
322
+ monitor_status=$(jq -r '.status' "$cfg")
323
+ [ "$monitor_status" = "stopped" ] && echo "Monitor '$name' stopped — loop terminated." && exit 0
324
+ [ "$monitor_status" = "paused" ] && echo "Monitor '$name' paused — will not reschedule." && exit 0
325
+
326
+ # Init state file if missing
327
+ [ ! -f "$state_file" ] && echo '{"processed_ids":{},"in_flight":[]}' > "$state_file"
328
+
329
+ max_concurrent=$(jq -r '.max_concurrent // 1' "$cfg")
330
+ agent_type=$(jq -r '.agent_type // "coder"' "$cfg")
331
+ poll_interval=$(jq -r '.poll_interval // 120' "$cfg")
332
+ ```
333
+
334
+ **In-flight guard** — if at capacity, reschedule and stop this tick:
335
+ ```bash
336
+ in_flight_count=$(jq '.in_flight // [] | length' "$state_file")
337
+ in_flight_count=${in_flight_count:-0}
338
+ if [ "$in_flight_count" -ge "$max_concurrent" ]; then
339
+ echo "[$name] In-flight ($in_flight_count) >= max_concurrent ($max_concurrent) — skipping claim this tick."
340
+ # Reschedule next tick — do this BEFORE exit so the loop survives
341
+ # ScheduleWakeup: delaySeconds=poll_interval, prompt="/mastermind:monitor --action tick --name <name>",
342
+ # reason="Monitor <name> in-flight throttle — will retry next tick"
343
+ exit 0
344
+ fi
345
+ ```
346
+
347
+ > **Note:** The ScheduleWakeup call above is pseudocode inside the comment. Claude must call the actual `ScheduleWakeup` tool at this point, then `exit 0`. The `exit 0` here is a bash signal that the tick logic description treats as "stop further processing in this tick"; it does NOT skip the ScheduleWakeup — that fires first.
348
+
349
+ **Check for retry-pending tasks (before polling sources):**
350
+
351
+ If a previous task failed but has retry_count < 3, it remains in `in_flight` with `status = "retry_pending"`. Re-execute it instead of polling for a new task:
352
+
353
+ ```bash
354
+ retry_task=$(jq -c '
355
+ .in_flight[] as $t |
356
+ (.processed_ids[($t.source_type + ":" + $t.external_id)].status // "") |
357
+ if . == "retry_pending" then $t else empty end' "$state_file" | head -1)
358
+ ```
359
+
360
+ If `retry_task` is non-empty, bind it as the active task and jump to Step 3:
361
+
362
+ ```bash
363
+ if [ -n "$retry_task" ]; then
364
+ task_json="$retry_task"
365
+ task_source_type=$(echo "$task_json" | jq -r '.source_type')
366
+ task_external_id=$(echo "$task_json" | jq -r '.external_id')
367
+ task_title=$(echo "$task_json" | jq -r '.title')
368
+ echo "[$name] Retrying task: $task_title (source=$task_source_type id=$task_external_id)"
369
+ # Proceed directly to Step 3 — skip source polling and the "After claim" section
370
+ fi
371
+ ```
372
+
373
+ When `retry_task` is non-empty, the source loop below is skipped entirely (guarded by `if [ -z "$retry_task" ]`). `task_claimed` remains `false`, so the "After claim" registration block is also bypassed — the task is already registered in `in_flight` from its original claim.
374
+
375
+ **Update last_tick:**
376
+ ```bash
377
+ tmp="${cfg}.tmp"
378
+ jq --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.last_tick = $ts' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
379
+ ```
380
+
381
+ **Emit dashboard event:**
382
+ ```bash
383
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
384
+ CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
385
+ SESSION_ID="monitor-${name}-$(date -u +%Y%m%dT%H%M%S)"
386
+ curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
387
+ -H "Content-Type: application/json" \
388
+ -d "$(jq -cn --arg sid "$SESSION_ID" --arg name "$name" \
389
+ '{type:"monitor:tick",session:$sid,monitor:$name,ts:(now*1000|floor)}')" || true
390
+ ```
391
+
392
+ **Poll each source (in order, stop after first claimable task found):**
393
+
394
+ Source polling is skipped when a retry task is available (guarded below). The loop, the `case` dispatch, and each adapter are all inside the `if [ -z "$retry_task" ]` block:
395
+
396
+ ```bash
397
+ task_claimed=false
398
+ task_json=""
399
+ if [ -z "$retry_task" ]; then
400
+ while IFS= read -r src; do
401
+ src_type=$(echo "$src" | jq -r '.type')
402
+ case "$src_type" in
403
+ linear)
404
+ # === Linear adapter (below) ===
405
+ ```
406
+
407
+ > The `case` block continues through all adapters. After the Filesystem adapter closes its inner loop and `fi`, add `esac` → `done < <(jq -c '.sources[]' "$cfg")` → `fi` (closing the `if [ -z "$retry_task" ]` guard). After the outer `fi`, check `$task_claimed` to decide whether to run the "After claim" section.
408
+
409
+ ---
410
+
411
+ #### Source Adapter: Linear
412
+
413
+ > **Before calling any `mcp__claude_ai_Linear__*` tool**, confirm availability with `ToolSearch` (`select:mcp__claude_ai_Linear__list_issues,mcp__claude_ai_Linear__save_issue,mcp__claude_ai_Linear__save_comment`). If the Linear MCP server is unavailable, skip this source for the current tick with a warning.
414
+
415
+ ```bash
416
+ # Extract filter fields from $src
417
+ _lin_team=$(echo "$src" | jq -r '.filter.team // ""')
418
+ _lin_assignee=$(echo "$src" | jq -r '.filter.assignees[0] // ""')
419
+ _lin_states=$(echo "$src" | jq -r '.filter.states // ["Todo"] | join(",")')
420
+ _lin_labels=$(echo "$src" | jq -r '.filter.labels // []')
421
+ ```
422
+
423
+ Use `mcp__claude_ai_Linear__list_issues` with:
424
+ - `teamId`: `$_lin_team`
425
+ - `assigneeId` / `assigneeEmail`: `$_lin_assignee` (if non-empty)
426
+ - State filter: resolved from `$_lin_states`
427
+ - Label filter applied client-side after fetch
428
+
429
+ Store the full list of returned issues as a JSON array in `$issues`. Then iterate with an explicit bash loop (same pattern as Monotask/Filesystem adapters):
430
+
431
+ ```bash
432
+ while IFS= read -r issue_json; do
433
+ [ -z "$issue_json" ] && continue
434
+
435
+ issue_id=$(echo "$issue_json" | jq -r '.id')
436
+ issue_title=$(echo "$issue_json" | jq -r '.title')
437
+ issue_desc=$(echo "$issue_json" | jq -r '.description // ""')
438
+ issue_url=$(echo "$issue_json" | jq -r '.url // ""')
439
+ issue_labels=$(echo "$issue_json" | jq -c '[(.labels // [])[] | .name]')
440
+ ```
441
+
442
+ 1. Check dedup — skip if already processed:
443
+ ```bash
444
+ already_processed=$(jq -r --arg id "linear:${issue_id}" '.processed_ids[$id] // empty' "$state_file")
445
+ [ -n "$already_processed" ] && continue
446
+ ```
447
+
448
+ 2. Client-side label filter — skip if issue doesn't match configured labels:
449
+ ```bash
450
+ if [ "$(echo "$_lin_labels" | jq 'length')" -gt 0 ]; then
451
+ has_label=$(jq -n --argjson want "$_lin_labels" --argjson got "$issue_labels" \
452
+ '($want - ($want - $got)) | length > 0')
453
+ [ "$has_label" != "true" ] && continue
454
+ fi
455
+ ```
456
+
457
+ 3. Build task object:
458
+ ```bash
459
+ task_json=$(jq -cn \
460
+ --arg eid "$issue_id" \
461
+ --arg title "$issue_title" \
462
+ --arg desc "$issue_desc" \
463
+ --arg url "$issue_url" \
464
+ --argjson labels "$issue_labels" \
465
+ '{
466
+ "source_type": "linear",
467
+ "external_id": $eid,
468
+ "title": $title,
469
+ "description": $desc,
470
+ "url": $url,
471
+ "labels": $labels
472
+ }')
473
+ ```
474
+
475
+ **Claim** (MCP — not bash): Call `mcp__claude_ai_Linear__save_issue` with:
476
+ - `issueId`: `$issue_id`
477
+ - Set state to "In Progress"
478
+ - Add label: `monitor:claimed`
479
+
480
+ **Progress comment** (MCP): Call `mcp__claude_ai_Linear__save_comment` with:
481
+ - `issueId`: `$issue_id`
482
+ - `body`: `"[Monitor: ${name}] Claimed by AI agent. Starting execution with agent type \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"`
483
+
484
+ ```bash
485
+ task_claimed=true
486
+ break 2 # break 2: exit per-issue loop AND outer source loop
487
+ done < <(echo "$issues" | jq -c '.[]') # close per-issue loop
488
+ ;; # end linear case branch
489
+ ```
490
+
491
+ ---
492
+
493
+ #### Source Adapter: GitHub Issues/PRs
494
+
495
+ ```bash
496
+ # Extract filter fields from $src
497
+ _gh_repo=$(echo "$src" | jq -r '.filter.repo // ""')
498
+ _gh_assignee=$(echo "$src" | jq -r '.filter.assignee // ""')
499
+ _gh_state=$(echo "$src" | jq -r '.filter.state // "open"')
500
+
501
+ # Build --label flags as an array (gh issue list takes one --label per flag, not comma-separated)
502
+ gh_label_args=()
503
+ while IFS= read -r _lbl; do
504
+ [ -n "$_lbl" ] && gh_label_args+=(--label "$_lbl")
505
+ done < <(echo "$src" | jq -r '(.filter.labels // [])[]')
506
+
507
+ # Poll via gh CLI — capture into $issues
508
+ issues=$(gh issue list \
509
+ --repo "$_gh_repo" \
510
+ ${_gh_assignee:+--assignee "$_gh_assignee"} \
511
+ "${gh_label_args[@]}" \
512
+ --state "$_gh_state" \
513
+ --json number,title,body,url,labels,assignees \
514
+ --limit 20)
515
+ ```
516
+
517
+ Iterate with an explicit bash loop:
518
+
519
+ ```bash
520
+ while IFS= read -r issue_json; do
521
+ [ -z "$issue_json" ] && continue
522
+
523
+ number=$(echo "$issue_json" | jq -r '.number')
524
+ ```
525
+
526
+ 1. Check dedup — TWO checks required:
527
+ ```bash
528
+ # Local dedup: skip if already in processed_ids
529
+ already_processed=$(jq -r --arg id "github:${number}" '.processed_ids[$id] // empty' "$state_file")
530
+ [ -n "$already_processed" ] && continue
531
+
532
+ # Remote dedup: skip if issue already has label monitor:claimed (externally labeled or other instance)
533
+ already_claimed=$(echo "$issue_json" | jq -r '[(.labels // [])[] | .name] | index("monitor:claimed")')
534
+ [ "$already_claimed" != "null" ] && continue
535
+ ```
536
+
537
+ 2. Build task object:
538
+ ```bash
539
+ task_json=$(jq -cn \
540
+ --argjson issue "$issue_json" \
541
+ --arg repo "$_gh_repo" \
542
+ '{
543
+ "source_type": "github",
544
+ "external_id": ($issue.number | tostring),
545
+ "title": $issue.title,
546
+ "description": ($issue.body // ""),
547
+ "url": $issue.url,
548
+ "repo": $repo,
549
+ "labels": [($issue.labels // [])[] | .name]
550
+ }')
551
+ ```
552
+
553
+ **Claim:** Run the GitHub Label Bootstrap (see end of file) using `$_gh_repo` before the first label operation — it is idempotent and only creates labels if missing. Then add labels:
554
+ ```bash
555
+ # Run GitHub Label Bootstrap here (see "GitHub Label Bootstrap" section) — idempotent, guarded
556
+ gh issue edit "$number" --repo "$_gh_repo" --add-label "monitor:claimed,monitor:in-progress"
557
+ ```
558
+
559
+ **Progress comment:**
560
+ ```bash
561
+ gh issue comment "$number" --repo "$_gh_repo" --body \
562
+ "[Monitor: ${name}] Claimed by AI agent. Executing with \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
563
+ ```
564
+
565
+ ```bash
566
+ task_claimed=true
567
+ break 2 # break 2: exit per-issue loop AND outer source loop
568
+ done < <(echo "$issues" | jq -c '.[]') # close per-issue loop
569
+ ;; # end github case branch
570
+ ```
571
+
572
+ **Stage labels** (applied progressively throughout execution):
573
+ - `monitor:claimed` → task picked up
574
+ - `monitor:in-progress` → agent is executing
575
+ - `monitor:review` → agent completed, awaiting human review (optional)
576
+ - `monitor:done` → fully complete
577
+ - `monitor:failed` → all retries exhausted
578
+
579
+ ---
580
+
581
+ #### Source Adapter: Monotask
582
+
583
+ ```bash
584
+ # Extract filter fields from $src
585
+ _mt_board=$(echo "$src" | jq -r '.filter.board // ""')
586
+ _mt_col=$(echo "$src" | jq -r '.filter.column // "Todo"')
587
+ _mt_label=$(echo "$src" | jq -r '.filter.label // "role:ai-agent"')
588
+
589
+ # Resolve board_id from board title
590
+ board_id=$(monotask board list --json | jq -r --arg t "$_mt_board" '.[] | select(.title==$t) | .id' | head -1)
591
+ if [ -z "$board_id" ]; then
592
+ echo "[monotask] Board '$_mt_board' not found — skipping source."
593
+ # skip to next source adapter
594
+ else
595
+
596
+ # Resolve column ids
597
+ cols=$(monotask column list "$board_id" --json)
598
+ todo_col=$(echo "$cols" | jq -r --arg t "$_mt_col" '.[] | select(.title==$t) | .id' | head -1)
599
+ doing_col=$(echo "$cols" | jq -r '.[] | select(.title=="Doing" or .title=="In Progress") | .id' | head -1)
600
+ done_col=$(echo "$cols" | jq -r '.[] | select(.title=="Done") | .id' | head -1)
601
+
602
+ # Poll: unclaimed cards in todo column with matching label
603
+ cards=$(monotask card list "$board_id" --col "$todo_col" --label "$_mt_label" --json \
604
+ | jq '[.[] | select((.labels // []) | index("claimed") | not)]')
605
+ ```
606
+
607
+ Iterate over cards with an explicit bash loop (same pattern as the filesystem adapter):
608
+
609
+ ```bash
610
+ while IFS= read -r card; do
611
+ [ -z "$card" ] && continue
612
+
613
+ card_id=$(echo "$card" | jq -r '.id')
614
+ card_title=$(echo "$card" | jq -r '.title')
615
+ ```
616
+
617
+ 1. Check dedup:
618
+ ```bash
619
+ already_processed=$(jq -r --arg id "monotask:${card_id}" '.processed_ids[$id] // empty' "$state_file")
620
+ [ -n "$already_processed" ] && continue
621
+ ```
622
+
623
+ **Build task object** (includes board/column IDs so Step 4 can re-extract them from `task_json` without relying on polling-scope shell vars):
624
+ ```bash
625
+ task_json=$(jq -cn \
626
+ --arg eid "$card_id" \
627
+ --arg title "$card_title" \
628
+ --arg board "$board_id" \
629
+ --arg doing "$doing_col" \
630
+ --arg done "$done_col" \
631
+ '{
632
+ "source_type": "monotask",
633
+ "external_id": $eid,
634
+ "title": $title,
635
+ "description": "",
636
+ "board_id": $board,
637
+ "doing_col": $doing,
638
+ "done_col": $done
639
+ }')
640
+ ```
641
+
642
+ **Claim:**
643
+ ```bash
644
+ monotask card move "$board_id" "$card_id" "$doing_col" --json
645
+ monotask card label add "$board_id" "$card_id" "claimed" --json
646
+ monotask card label add "$board_id" "$card_id" "monitor:in-progress" --json
647
+ ```
648
+
649
+ **Progress comment:**
650
+ ```bash
651
+ monotask card comment add "$board_id" "$card_id" \
652
+ "[Monitor: ${name}] Claimed by AI agent. Executing with \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
653
+
654
+ task_claimed=true
655
+ break 2 # break 2: exit per-card loop AND outer source loop
656
+ done < <(echo "$cards" | jq -c '.[]') # close per-card loop
657
+ fi # close 'if [ -z "$board_id" ]' else block
658
+ ;; # end monotask case branch
659
+ ```
660
+
661
+ ---
662
+
663
+ #### Source Adapter: Filesystem
664
+
665
+ ```bash
666
+ # Extract filter fields from $src
667
+ folder=$(echo "$src" | jq -r '.filter.folder // "./tasks"')
668
+
669
+ if [ ! -d "$folder" ]; then
670
+ echo "[filesystem] Folder '$folder' not found — skipping source."
671
+ # skip to next source adapter
672
+ else
673
+
674
+ # Poll: find unclaimed .task files
675
+ tasks=$(find "$folder" -name "*.task" -not -name "*.claimed" -not -name "*.done" -not -name "*.failed" 2>/dev/null)
676
+
677
+ # Iterate — stop after first claimable file
678
+ while IFS= read -r task_file; do
679
+ [ -z "$task_file" ] && continue
680
+ ```
681
+
682
+ (All per-file steps are inside this while loop; close with `done <<< "$tasks"` after the claim. Then close the `else` block with `fi`.)
683
+
684
+ For each `.task` file (`task_file` = absolute path):
685
+ 1. Check dedup:
686
+ ```bash
687
+ already_processed=$(jq -r --arg id "filesystem:${task_file}" '.processed_ids[$id] // empty' "$state_file")
688
+ [ -n "$already_processed" ] && continue
689
+ ```
690
+ 2. Read task content:
691
+ ```bash
692
+ task_title=$(head -1 "$task_file")
693
+ task_description=$(tail -n +2 "$task_file")
694
+ task_base="${task_file%.task}"
695
+ task_json=$(jq -cn \
696
+ --arg title "$task_title" \
697
+ --arg description "$task_description" \
698
+ --arg eid "$task_file" \
699
+ --arg base_path "$task_base" \
700
+ '{
701
+ "source_type": "filesystem",
702
+ "external_id": $eid,
703
+ "title": $title,
704
+ "description": $description,
705
+ "base_path": $base_path
706
+ }')
707
+ ```
708
+
709
+ **Claim:**
710
+ ```bash
711
+ mv "${task_file}" "${task_base}.task.claimed"
712
+ ```
713
+
714
+ **Progress log:**
715
+ ```bash
716
+ echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — Claimed. Executing with ${agent_type}." \
717
+ >> "${task_base}.task.log"
718
+ ```
719
+
720
+ ```bash
721
+ task_claimed=true
722
+ break 2 # break 2: exit per-file loop AND outer source loop
723
+ done <<< "$tasks" # close per-file while loop (reached only if no file was claimed)
724
+ fi # close 'if [ ! -d "$folder" ]' else block
725
+ ;; # end filesystem case branch
726
+ esac
727
+ done < <(jq -c '.sources[]' "$cfg")
728
+ fi # close 'if [ -z "$retry_task" ]' guard
729
+ # After: if $task_claimed=true, proceed to "After claim"; otherwise skip to Step 5
730
+ ```
731
+
732
+ **Stage files:** `.task` → `.task.claimed` → `.task.done` or `.task.failed`
733
+
734
+ ---
735
+
736
+ #### After a task is claimed (all sources — common code)
737
+
738
+ This section runs ONLY when `task_claimed = true` (a task was found and claimed in the source loop above). The retry path sets `task_json=$retry_task` and jumps directly to Step 3, bypassing this section entirely.
739
+
740
+ ```bash
741
+ if [ "$task_claimed" = "true" ]; then
742
+ ```
743
+
744
+ Each source adapter MUST have set `task_json` to a complete JSON object before `task_claimed=true`. The full task object includes all fields used by Step 3 and Step 4 for that source type.
745
+
746
+ **Extract task metadata (must run first, before all blocks below):**
747
+ ```bash
748
+ task_source_type=$(echo "$task_json" | jq -r '.source_type')
749
+ task_external_id=$(echo "$task_json" | jq -r '.external_id')
750
+ task_title=$(echo "$task_json" | jq -r '.title')
751
+ ```
752
+
753
+ **Register in state as in-flight** (stores full task object for retry replay):
754
+ ```bash
755
+ tmp="${state_file}.tmp"
756
+ jq --argjson task "$task_json" \
757
+ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
758
+ '.in_flight += [$task + {"claimed_at": $ts, "retry_count": 0}]' \
759
+ "$state_file" > "$tmp" && mv "$tmp" "$state_file"
760
+ ```
761
+
762
+ **Mark as processed in dedup index:**
763
+ ```bash
764
+ tmp="${state_file}.tmp"
765
+ jq --arg key "${task_source_type}:${task_external_id}" \
766
+ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
767
+ '.processed_ids[$key] = {status: "in_flight", claimed_at: $ts, retry_count: 0}' \
768
+ "$state_file" > "$tmp" && mv "$tmp" "$state_file"
769
+ ```
770
+
771
+ **Increment stats:**
772
+ ```bash
773
+ tmp="${cfg}.tmp"
774
+ jq '.stats.total_claimed += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
775
+ fi # end 'if [ "$task_claimed" = "true" ]'
776
+ ```
777
+
778
+ ---
779
+
780
+ ## Step 3 — Execute the Task
781
+
782
+ **Guard:** Only execute Step 3 and Step 4 when a task was actually claimed or retried. On idle ticks (no task available, no retry pending), skip directly to Step 5.
783
+
784
+ ```bash
785
+ if [ -n "$task_json" ]; then
786
+ ```
787
+
788
+ Spawn a Task agent to do the actual work. This runs **synchronously** (`run_in_background: false`) so we can capture the result and update status.
789
+
790
+ The return value of the `Task` tool call is the agent's full text output. Capture it into `agent_output`:
791
+
792
+ ```javascript
793
+ agent_output = Task({
794
+ subagent_type: agent_type, // from monitor config
795
+ description: `Monitor "${name}" executing: ${task.title}`,
796
+ run_in_background: false,
797
+ prompt: `You are an AI agent executing a task claimed by the Mastermind Monitor "${name}".
798
+
799
+ TASK: ${task.title}
800
+
801
+ DESCRIPTION:
802
+ ${task.description || "(no description provided)"}
803
+
804
+ SOURCE: ${task.source_type} | ID: ${task.external_id}
805
+ URL: ${task.url || "n/a"}
806
+
807
+ INSTRUCTIONS:
808
+ 1. Understand the task from the title and description above.
809
+ 2. Execute the task fully and completely using available tools.
810
+ 3. For code tasks: read relevant files, make changes, run tests if possible.
811
+ 4. For research tasks: gather information, synthesize findings.
812
+ 5. Produce a clear result summary at the end.
813
+
814
+ OUTPUT FORMAT:
815
+ At the end of your work, output a JSON block EXACTLY like this:
816
+ \`\`\`json
817
+ {
818
+ "status": "done|failed",
819
+ "summary": "one paragraph summary of what was accomplished",
820
+ "artifacts": ["list of files changed or created"],
821
+ "next_actions": ["any follow-up suggestions"]
822
+ }
823
+ \`\`\`
824
+
825
+ If you encounter an error you cannot recover from, set status to "failed" and explain in summary.`
826
+ })
827
+ ```
828
+
829
+ `agent_output` is the full text returned by the Task call. Parse the JSON block and extract the task metadata for use in Step 4. The metadata vars (`task_source_type`, `task_external_id`, `task_title`) are already set by the "After claim" common code for new tasks, and by the retry branch for retried tasks — this block re-extracts them from `$task_json` as a safety guard:
830
+
831
+ ```bash
832
+ # Task metadata (safe re-extract — already set by claim/retry path)
833
+ task_source_type=$(echo "$task_json" | jq -r '.source_type')
834
+ task_external_id=$(echo "$task_json" | jq -r '.external_id')
835
+ task_title=$(echo "$task_json" | jq -r '.title')
836
+
837
+ # Parse result JSON block from agent_output (all three fields in one python pass)
838
+ _result_json=$(echo "$agent_output" | python3 -c "
839
+ import sys, re, json
840
+ m = re.search(r'\`\`\`json\s*(\{.*?\})\s*\`\`\`', sys.stdin.read(), re.DOTALL)
841
+ print(m.group(1) if m else '{}')" 2>/dev/null || echo '{}')
842
+
843
+ result_status=$(echo "$_result_json" | jq -r '.status // "failed"')
844
+ result_summary=$(echo "$_result_json" | jq -r '.summary // "(no summary)"')
845
+ result_artifacts=$(echo "$_result_json" | jq -r '(.artifacts // []) | join(", ")')
846
+ result_next_actions=$(echo "$_result_json" | jq -r '(.next_actions // []) | join(", ")')
847
+ ```
848
+
849
+ ---
850
+
851
+ ## Step 4 — Handle Result
852
+
853
+ Branch on `result_status`:
854
+
855
+ ```bash
856
+ if [ "$result_status" = "done" ]; then
857
+ ```
858
+
859
+ ### On success (`status: "done"`)
860
+
861
+ **Post completion comment/update per source** — dispatch on `$task_source_type`:
862
+
863
+ **Linear** (MCP tool calls — not bash; load Linear MCP schema via ToolSearch first): for `task_source_type=linear`:
864
+
865
+ Call `mcp__claude_ai_Linear__save_comment` with:
866
+ - `issueId`: `$task_external_id`
867
+ - `body`: pass the following as a real multi-line string (use actual newline characters — do NOT use `\n` escape sequences per the Linear MCP server requirement):
868
+
869
+ ```
870
+ [Monitor: ${name}] ✓ Task complete.
871
+
872
+ Summary: ${result_summary}
873
+
874
+ Artifacts: ${result_artifacts}
875
+ Next actions: ${result_next_actions}
876
+
877
+ Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ) | Agent: ${agent_type}
878
+ ```
879
+
880
+ Then call `mcp__claude_ai_Linear__save_issue` with:
881
+ - `issueId`: `$task_external_id`
882
+ - `stateId`: resolved ID for state name "Done" in this team
883
+
884
+ **GitHub, Monotask, Filesystem** — dispatched via `case`:
885
+ ```bash
886
+ case "$task_source_type" in
887
+ github)
888
+ task_repo=$(echo "$task_json" | jq -r '.repo')
889
+ gh issue comment "$task_external_id" --repo "$task_repo" --body \
890
+ "[Monitor: ${name}] Task complete.
891
+
892
+ Summary: ${result_summary}
893
+
894
+ Artifacts: ${result_artifacts}
895
+ Next actions: ${result_next_actions}
896
+ Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
897
+
898
+ gh issue edit "$task_external_id" --repo "$task_repo" \
899
+ --remove-label "monitor:in-progress" \
900
+ --add-label "monitor:done"
901
+
902
+ # If it was a "complete this task" issue, close it:
903
+ # gh issue close "$task_external_id" --repo "$task_repo" --comment "Closed by monitor after completion."
904
+ ;;
905
+ monotask)
906
+ # Re-extract from task_json — polling-scope vars are not reliable on retry path
907
+ board_id=$(echo "$task_json" | jq -r '.board_id')
908
+ card_id=$(echo "$task_json" | jq -r '.external_id')
909
+ done_col=$(echo "$task_json" | jq -r '.done_col')
910
+
911
+ monotask card comment add "$board_id" "$card_id" \
912
+ "[Monitor: ${name}] Task complete.
913
+
914
+ Summary: ${result_summary}
915
+
916
+ Artifacts: ${result_artifacts}
917
+ Next actions: ${result_next_actions}
918
+ Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
919
+
920
+ monotask card move "$board_id" "$card_id" "$done_col" --json
921
+ monotask card label remove "$board_id" "$card_id" "monitor:in-progress" --json
922
+ monotask card label add "$board_id" "$card_id" "monitor:done" --json
923
+ ;;
924
+ filesystem)
925
+ task_base=$(echo "$task_json" | jq -r '.base_path')
926
+ echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — DONE." >> "${task_base}.task.log"
927
+ echo "Summary: ${result_summary}" >> "${task_base}.task.log"
928
+ echo "Artifacts: ${result_artifacts}" >> "${task_base}.task.log"
929
+ echo "Next actions: ${result_next_actions}" >> "${task_base}.task.log"
930
+ mv "${task_base}.task.claimed" "${task_base}.task.done"
931
+ ;;
932
+ esac # end task_source_type dispatch (success path)
933
+ ```
934
+
935
+ **Update state — remove from in-flight, mark done:**
936
+ ```bash
937
+ tmp="${state_file}.tmp"
938
+ jq --arg key "${task_source_type}:${task_external_id}" \
939
+ --arg eid "$task_external_id" \
940
+ --arg stype "$task_source_type" \
941
+ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
942
+ '(.processed_ids[$key].status = "done") |
943
+ (.processed_ids[$key].done_at = $ts) |
944
+ (.in_flight = [.in_flight[] | select(.external_id != $eid or .source_type != $stype)])' \
945
+ "$state_file" > "$tmp" && mv "$tmp" "$state_file"
946
+
947
+ tmp="${cfg}.tmp"
948
+ jq '.stats.done += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
949
+ ```
950
+
951
+ **Emit dashboard event:**
952
+ ```bash
953
+ curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
954
+ -H "Content-Type: application/json" \
955
+ -d "$(jq -cn --arg sid "$SESSION_ID" --arg name "$name" --arg title "$task_title" \
956
+ '{type:"monitor:task:done",session:$sid,monitor:$name,task:$title,ts:(now*1000|floor)}')" || true
957
+ ```
958
+
959
+ ```bash
960
+ else # result_status != "done"
961
+ ```
962
+
963
+ ### On failure
964
+
965
+ **Retry logic (max 3 attempts):**
966
+
967
+ ```bash
968
+ retry_count=$(jq -r --arg key "${task_source_type}:${task_external_id}" \
969
+ '.processed_ids[$key].retry_count // 0' "$state_file")
970
+
971
+ if [ "$retry_count" -lt 3 ]; then
972
+ new_retry=$((retry_count + 1))
973
+ echo "[$name] Task failed (attempt $new_retry/3). Will retry next tick."
974
+
975
+ # Update retry count in state
976
+ tmp="${state_file}.tmp"
977
+ jq --arg key "${task_source_type}:${task_external_id}" \
978
+ --arg eid "$task_external_id" \
979
+ --arg stype "$task_source_type" \
980
+ --argjson r "$new_retry" \
981
+ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
982
+ '(.processed_ids[$key].retry_count = $r) |
983
+ (.processed_ids[$key].last_failure = $ts) |
984
+ (.processed_ids[$key].status = "retry_pending") |
985
+ (.in_flight = [.in_flight[] |
986
+ if (.external_id == $eid and .source_type == $stype)
987
+ then .retry_count = $r | .last_failure = $ts
988
+ else . end])' \
989
+ "$state_file" > "$tmp" && mv "$tmp" "$state_file"
990
+
991
+ tmp="${cfg}.tmp"
992
+ jq '.stats.retried += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
993
+
994
+ # Post retry comment per source
995
+ _retry_msg="[Monitor: ${name}] Task failed (attempt ${new_retry}/3). Will retry next tick. Error: ${result_summary}"
996
+ # Linear (MCP — not bash): call mcp__claude_ai_Linear__save_comment with
997
+ # issueId=$task_external_id body="$_retry_msg"
998
+ # Then call mcp__claude_ai_Linear__save_issue to set state back to "In Progress".
999
+
1000
+ case "$task_source_type" in
1001
+ github)
1002
+ task_repo=$(echo "$task_json" | jq -r '.repo')
1003
+ gh issue comment "$task_external_id" --repo "$task_repo" --body "$_retry_msg" 2>/dev/null || true
1004
+ ;;
1005
+ monotask)
1006
+ _board=$(echo "$task_json" | jq -r '.board_id')
1007
+ monotask card comment add "$_board" "$task_external_id" "$_retry_msg" 2>/dev/null || true
1008
+ ;;
1009
+ filesystem)
1010
+ _base=$(echo "$task_json" | jq -r '.base_path')
1011
+ echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — RETRY ${new_retry}/3. ${result_summary}" >> "${_base}.task.log"
1012
+ ;;
1013
+ esac
1014
+
1015
+ else
1016
+ echo "[$name] Task failed after 3 attempts. Marking as failed."
1017
+
1018
+ # Post final failure comment and set external status per source
1019
+ _fail_msg="[Monitor: ${name}] Task failed after 3 attempts. Manual intervention required.
1020
+ Error: ${result_summary}
1021
+ Last attempt: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
1022
+
1023
+ # Linear (MCP — not bash): call mcp__claude_ai_Linear__save_comment with
1024
+ # issueId=$task_external_id body="$_fail_msg"
1025
+ # Then call mcp__claude_ai_Linear__save_issue to add label "monitor:failed"
1026
+ # and set state to "Cancelled" or "Blocked" as appropriate for the team.
1027
+
1028
+ case "$task_source_type" in
1029
+ github)
1030
+ task_repo=$(echo "$task_json" | jq -r '.repo')
1031
+ gh issue comment "$task_external_id" --repo "$task_repo" --body "$_fail_msg" 2>/dev/null || true
1032
+ gh issue edit "$task_external_id" --repo "$task_repo" \
1033
+ --add-label "monitor:failed" --remove-label "monitor:in-progress" 2>/dev/null || true
1034
+ ;;
1035
+ monotask)
1036
+ _board=$(echo "$task_json" | jq -r '.board_id')
1037
+ monotask card comment add "$_board" "$task_external_id" "$_fail_msg" 2>/dev/null || true
1038
+ monotask card label remove "$_board" "$task_external_id" "monitor:in-progress" 2>/dev/null || true
1039
+ monotask card label add "$_board" "$task_external_id" "monitor:failed" 2>/dev/null || true
1040
+ ;;
1041
+ filesystem)
1042
+ _base=$(echo "$task_json" | jq -r '.base_path')
1043
+ echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — FAILED after 3 attempts. ${result_summary}" >> "${_base}.task.log"
1044
+ mv "${_base}.task.claimed" "${_base}.task.failed" 2>/dev/null || true
1045
+ ;;
1046
+ esac
1047
+
1048
+ # Update state
1049
+ tmp="${state_file}.tmp"
1050
+ jq --arg key "${task_source_type}:${task_external_id}" \
1051
+ --arg eid "$task_external_id" \
1052
+ --arg stype "$task_source_type" \
1053
+ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
1054
+ '(.processed_ids[$key].status = "failed") |
1055
+ (.processed_ids[$key].failed_at = $ts) |
1056
+ (.in_flight = [.in_flight[] | select(.external_id != $eid or .source_type != $stype)])' \
1057
+ "$state_file" > "$tmp" && mv "$tmp" "$state_file"
1058
+
1059
+ tmp="${cfg}.tmp"
1060
+ jq '.stats.failed += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
1061
+ fi # end permanent-fail branch (if retry_count -lt 3 ... else ... fi)
1062
+ fi # end result_status branch (if result_status = "done" ... else ... fi)
1063
+
1064
+ fi # end task guard (if [ -n "$task_json" ]; only runs Steps 3-4 when a task was claimed or retried)
1065
+ ```
1066
+
1067
+ ---
1068
+
1069
+ ## Step 5 — Reschedule Next Tick
1070
+
1071
+ Always the last step, regardless of whether a task was found/executed.
1072
+
1073
+ Call `ScheduleWakeup` with:
1074
+ - `delaySeconds`: value of `poll_interval` from config (default 120)
1075
+ - `prompt`: `/mastermind:monitor --action tick --name <name>`
1076
+ - `reason`: `Monitor <name> polling every <poll_interval>s (<N sources>)`
1077
+
1078
+ This is what makes the monitor run forever. The only way to stop it is to set `status=stopped` or `status=paused` — the tick checks this at the top of Step 2.
1079
+
1080
+ ---
1081
+
1082
+ ## Step 6 — Return Output
1083
+
1084
+ ```yaml
1085
+ domain: ops
1086
+ status: complete
1087
+ action: <action>
1088
+ monitor: <name>
1089
+ tasks_this_tick: <0 or 1>
1090
+ task_result: <done|failed|retry_pending|none>
1091
+ next_tick_in: <poll_interval>s
1092
+ state_file: .monomind/monitor/<name>-state.json
1093
+ run_id: <SESSION_ID>
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ ## Step 7 — Brain Write (standalone only)
1099
+
1100
+ If `caller` is not "command", follow `_protocol.md` Brain Write Procedure for domain `ops`.
1101
+
1102
+ ---
1103
+
1104
+ ## Configuration File Schemas
1105
+
1106
+ ### `.monomind/monitor/<name>.json`
1107
+
1108
+ ```json
1109
+ {
1110
+ "name": "dev-agent",
1111
+ "status": "active",
1112
+ "poll_interval": 120,
1113
+ "agent_type": "coder",
1114
+ "max_concurrent": 1,
1115
+ "sources": [
1116
+ {
1117
+ "type": "linear",
1118
+ "filter": {
1119
+ "team": "ENG",
1120
+ "assignees": ["morteza@agent-f.com"],
1121
+ "states": ["Todo"],
1122
+ "labels": ["ai-agent"],
1123
+ "project": ""
1124
+ }
1125
+ },
1126
+ {
1127
+ "type": "github",
1128
+ "filter": {
1129
+ "repo": "nokhodian/monomind",
1130
+ "assignee": "nokhodian",
1131
+ "labels": ["ai-agent"],
1132
+ "state": "open",
1133
+ "type": "issue"
1134
+ }
1135
+ },
1136
+ {
1137
+ "type": "monotask",
1138
+ "filter": {
1139
+ "board": "monomind-tasks-dev",
1140
+ "column": "Todo",
1141
+ "label": "role:ai-agent"
1142
+ }
1143
+ },
1144
+ {
1145
+ "type": "filesystem",
1146
+ "filter": {
1147
+ "folder": "./tasks",
1148
+ "glob": "*.task",
1149
+ "user": ""
1150
+ }
1151
+ }
1152
+ ],
1153
+ "stats": {
1154
+ "done": 0,
1155
+ "failed": 0,
1156
+ "retried": 0,
1157
+ "total_claimed": 0
1158
+ },
1159
+ "created_at": "2026-05-18T00:00:00Z",
1160
+ "last_tick": null
1161
+ }
1162
+ ```
1163
+
1164
+ ### `.monomind/monitor/<name>-state.json`
1165
+
1166
+ ```json
1167
+ {
1168
+ "processed_ids": {
1169
+ "linear:LIN-123": {
1170
+ "status": "done",
1171
+ "claimed_at": "2026-05-18T10:00:00Z",
1172
+ "done_at": "2026-05-18T10:05:00Z",
1173
+ "retry_count": 0
1174
+ },
1175
+ "github:456": {
1176
+ "status": "failed",
1177
+ "claimed_at": "2026-05-18T09:00:00Z",
1178
+ "failed_at": "2026-05-18T09:20:00Z",
1179
+ "retry_count": 3
1180
+ }
1181
+ },
1182
+ "in_flight": [
1183
+ {
1184
+ "source_type": "monotask",
1185
+ "external_id": "abc-uuid",
1186
+ "title": "Fix auth bug",
1187
+ "description": "",
1188
+ "board_id": "board-uuid-here",
1189
+ "doing_col": "doing-col-uuid",
1190
+ "done_col": "done-col-uuid",
1191
+ "claimed_at": "2026-05-18T10:10:00Z",
1192
+ "retry_count": 1,
1193
+ "last_failure": "2026-05-18T10:12:00Z"
1194
+ }
1195
+ ]
1196
+ }
1197
+ ```
1198
+
1199
+ ---
1200
+
1201
+ ## Quick Reference — Common Commands
1202
+
1203
+ ```bash
1204
+ # Create a monitor watching GitHub issues assigned to you
1205
+ /mastermind:monitor --action start --name dev-watcher \
1206
+ --source github --project nokhodian/monomind --user nokhodian \
1207
+ --label ai-agent --agent coder --interval 120
1208
+
1209
+ # Add a Linear source to an existing monitor
1210
+ /mastermind:monitor --action add-source --name dev-watcher \
1211
+ --source linear --team ENG --user morteza@agent-f.com \
1212
+ --state Todo --label ai-agent
1213
+
1214
+ # Add a monotask board source
1215
+ /mastermind:monitor --action add-source --name dev-watcher \
1216
+ --source monotask --project monomind-tasks-dev \
1217
+ --state Todo --label role:ai-agent
1218
+
1219
+ # Add a filesystem folder source
1220
+ /mastermind:monitor --action add-source --name dev-watcher \
1221
+ --source filesystem --folder ./tasks
1222
+
1223
+ # Check status
1224
+ /mastermind:monitor --action status --name dev-watcher
1225
+
1226
+ # List all monitors
1227
+ /mastermind:monitor
1228
+
1229
+ # Pause (stops auto-rescheduling after current tick)
1230
+ /mastermind:monitor --action pause --name dev-watcher
1231
+
1232
+ # Resume (re-enters the tick loop)
1233
+ /mastermind:monitor --action resume --name dev-watcher
1234
+
1235
+ # Stop permanently
1236
+ /mastermind:monitor --action stop --name dev-watcher
1237
+ ```
1238
+
1239
+ ---
1240
+
1241
+ ## GitHub Label Bootstrap
1242
+
1243
+ Run this once, inline in the GitHub claim path, immediately before the first `gh issue edit` call. At claim time, `$_gh_repo` is in scope (extracted from `$src` at the top of the GitHub adapter). Guard with a check so it only runs when the `monitor:claimed` label is absent:
1244
+
1245
+ ```bash
1246
+ # Bootstrap monitor labels on first claim (idempotent — --force handles existing labels)
1247
+ if ! gh label list --repo "$_gh_repo" --json name 2>/dev/null \
1248
+ | jq -e '.[] | select(.name=="monitor:claimed")' > /dev/null 2>&1; then
1249
+ for label in "monitor:claimed" "monitor:in-progress" "monitor:review" "monitor:done" "monitor:failed"; do
1250
+ # shasum is available on both macOS and Linux; md5sum is Linux-only
1251
+ color=$(printf '%s' "$label" | shasum | cut -c1-6)
1252
+ gh label create "$label" --repo "$_gh_repo" --color "$color" --force 2>/dev/null || true
1253
+ done
1254
+ fi
1255
+ ```
1256
+
1257
+ ---
1258
+
1259
+ ## Important Behavioral Notes
1260
+
1261
+ **`poll_interval` is idle gap, not heartbeat.** Because `tick` waits synchronously for the agent to finish before rescheduling (Step 3 uses `run_in_background: false`), `poll_interval` is the *minimum idle time between task claims* — not a wall-clock polling frequency. A user who sets `--interval 60` and has tasks that take 10 minutes will get one task claimed every ~10 minutes, not every 60 seconds.
1262
+
1263
+ **`max_concurrent` is a per-tick claim limit.** Since the default is 1 and the tick blocks on the agent, this means at most one task runs per cycle. Increasing it allows the monitor to claim and spawn multiple tasks per tick — but all still block the tick sequentially unless the Task calls use `run_in_background: true` (which requires manual result tracking).
1264
+
1265
+ **Retry-pending tasks are prioritized over new claims.** On each tick, the monitor first checks `in_flight` for any `retry_pending` entries and re-executes them before polling sources for new work. This ensures stuck tasks don't accumulate.
1266
+
1267
+ ---
1268
+
1269
+ ## Notes on Filter Precedence
1270
+
1271
+ When multiple users or states are specified (via repeated `--user` or `--state` flags), the monitor ORs them:
1272
+ - `--user alice --user bob` → fetch tasks assigned to alice OR bob
1273
+ - `--state Todo --state "In Progress"` → fetch tasks in either state
1274
+
1275
+ The `--label` flag is ANDed with user/state: only tasks matching the label AND the user/state filter are claimed.