monomind 1.10.41 → 1.10.43
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/.claude/commands/mastermind/architect.md +1 -1
- package/.claude/commands/mastermind/brain.md +1 -1
- package/.claude/commands/mastermind/build.md +1 -1
- package/.claude/commands/mastermind/content.md +1 -1
- package/.claude/commands/mastermind/finance.md +1 -1
- package/.claude/commands/mastermind/idea.md +1 -1
- package/.claude/commands/mastermind/marketing.md +1 -1
- package/.claude/commands/mastermind/ops.md +1 -1
- package/.claude/commands/mastermind/release.md +1 -1
- package/.claude/commands/mastermind/research.md +1 -1
- package/.claude/commands/mastermind/review.md +1 -1
- package/.claude/commands/mastermind/sales.md +1 -1
- package/.claude/commands/mastermind/techport.md +1 -1
- package/.claude/commands/monomind/idea.md +1 -1
- package/.claude/commands/monomind/improve.md +1 -1
- package/.claude/commands/monomind/review.md +1 -1
- package/.claude/helpers/skill-registry.json +26 -0
- package/.claude/skills/mastermind/_repeat.md +141 -0
- package/.claude/skills/mastermind/monitor.md +1284 -0
- package/package.json +1 -1
- package/packages/@monomind/cli/dist/src/init/executor.js +16 -8
- package/packages/@monomind/cli/package.json +1 -1
|
@@ -0,0 +1,1284 @@
|
|
|
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 / $label from first value (adapters that take one value)
|
|
256
|
+
user="${users%% *}"
|
|
257
|
+
state="${states%% *}"
|
|
258
|
+
label="${labels%% *}"
|
|
259
|
+
|
|
260
|
+
case "$source" in
|
|
261
|
+
linear)
|
|
262
|
+
assignees_json=$([ -n "$users" ] && printf '%s\n' $users | jq -R . | jq -sc '.' || echo '[]')
|
|
263
|
+
states_json=$([ -n "$states" ] && printf '%s\n' $states | jq -R . | jq -sc '.' || echo '["Todo"]')
|
|
264
|
+
labels_json=$([ -n "$labels" ] && printf '%s\n' $labels | jq -R . | jq -sc '.' || echo '[]')
|
|
265
|
+
src_json=$(jq -cn \
|
|
266
|
+
--arg team "${team:-}" \
|
|
267
|
+
--arg project "${project:-}" \
|
|
268
|
+
--argjson assignees "$assignees_json" \
|
|
269
|
+
--argjson states "$states_json" \
|
|
270
|
+
--argjson labels "$labels_json" \
|
|
271
|
+
'{"type":"linear","filter":{"team":$team,"assignees":$assignees,"states":$states,"labels":$labels,"project":$project}}')
|
|
272
|
+
;;
|
|
273
|
+
github)
|
|
274
|
+
labels_json=$([ -n "$labels" ] && printf '%s\n' $labels | jq -R . | jq -sc '.' || echo '[]')
|
|
275
|
+
src_json=$(jq -cn \
|
|
276
|
+
--arg repo "${project:-}" \
|
|
277
|
+
--arg assignee "${user:-}" \
|
|
278
|
+
--argjson labels "$labels_json" \
|
|
279
|
+
--arg state "${state:-open}" \
|
|
280
|
+
'{"type":"github","filter":{"repo":$repo,"assignee":$assignee,"labels":$labels,"state":$state,"type":"issue"}}')
|
|
281
|
+
;;
|
|
282
|
+
monotask)
|
|
283
|
+
src_json=$(jq -cn \
|
|
284
|
+
--arg board "${project:-}" \
|
|
285
|
+
--arg column "${state:-Todo}" \
|
|
286
|
+
--arg label "${label:-role:ai-agent}" \
|
|
287
|
+
'{"type":"monotask","filter":{"board":$board,"column":$column,"label":$label}}')
|
|
288
|
+
;;
|
|
289
|
+
filesystem)
|
|
290
|
+
src_json=$(jq -cn \
|
|
291
|
+
--arg folder "${folder:-./tasks}" \
|
|
292
|
+
--arg user "${user:-}" \
|
|
293
|
+
'{"type":"filesystem","filter":{"folder":$folder,"glob":"*.task","user":$user}}')
|
|
294
|
+
;;
|
|
295
|
+
*)
|
|
296
|
+
echo "Unknown source type: $source. Supported: linear | github | monotask | filesystem"
|
|
297
|
+
exit 1
|
|
298
|
+
;;
|
|
299
|
+
esac
|
|
300
|
+
|
|
301
|
+
tmp="${cfg}.tmp"
|
|
302
|
+
jq --argjson src "$src_json" '.sources += [$src]' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
303
|
+
echo "Source added to monitor '$name'."
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### `tick` — The Main Execution Loop
|
|
309
|
+
|
|
310
|
+
This is the heart of the monitor. Called by `ScheduleWakeup` every `poll_interval` seconds.
|
|
311
|
+
|
|
312
|
+
> **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.
|
|
313
|
+
|
|
314
|
+
**Required:** `--name`
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
cfg="$MONITOR_DIR/${name}.json"
|
|
318
|
+
state_file="$MONITOR_DIR/${name}-state.json"
|
|
319
|
+
|
|
320
|
+
# Load config
|
|
321
|
+
[ ! -f "$cfg" ] && echo "Monitor '$name' not found — loop terminated." && exit 0
|
|
322
|
+
|
|
323
|
+
monitor_status=$(jq -r '.status' "$cfg")
|
|
324
|
+
[ "$monitor_status" = "stopped" ] && echo "Monitor '$name' stopped — loop terminated." && exit 0
|
|
325
|
+
[ "$monitor_status" = "paused" ] && echo "Monitor '$name' paused — will not reschedule." && exit 0
|
|
326
|
+
|
|
327
|
+
# Init state file if missing
|
|
328
|
+
[ ! -f "$state_file" ] && echo '{"processed_ids":{},"in_flight":[]}' > "$state_file"
|
|
329
|
+
|
|
330
|
+
max_concurrent=$(jq -r '.max_concurrent // 1' "$cfg")
|
|
331
|
+
agent_type=$(jq -r '.agent_type // "coder"' "$cfg")
|
|
332
|
+
poll_interval=$(jq -r '.poll_interval // 120' "$cfg")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**In-flight guard** — if at capacity, reschedule and stop this tick:
|
|
336
|
+
```bash
|
|
337
|
+
in_flight_count=$(jq '.in_flight // [] | length' "$state_file")
|
|
338
|
+
in_flight_count=${in_flight_count:-0}
|
|
339
|
+
if [ "$in_flight_count" -ge "$max_concurrent" ]; then
|
|
340
|
+
echo "[$name] In-flight ($in_flight_count) >= max_concurrent ($max_concurrent) — skipping claim this tick."
|
|
341
|
+
# Reschedule next tick — do this BEFORE exit so the loop survives
|
|
342
|
+
# ScheduleWakeup: delaySeconds=poll_interval, prompt="/mastermind:monitor --action tick --name <name>",
|
|
343
|
+
# reason="Monitor <name> in-flight throttle — will retry next tick"
|
|
344
|
+
exit 0
|
|
345
|
+
fi
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
> **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.
|
|
349
|
+
|
|
350
|
+
**Check for retry-pending tasks (before polling sources):**
|
|
351
|
+
|
|
352
|
+
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:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
retry_task=$(jq -c '
|
|
356
|
+
.in_flight[] as $t |
|
|
357
|
+
(.processed_ids[($t.source_type + ":" + $t.external_id)].status // "") |
|
|
358
|
+
if . == "retry_pending" then $t else empty end' "$state_file" | head -1)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
If `retry_task` is non-empty, bind it as the active task and jump to Step 3:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
if [ -n "$retry_task" ]; then
|
|
365
|
+
task_json="$retry_task"
|
|
366
|
+
task_source_type=$(echo "$task_json" | jq -r '.source_type')
|
|
367
|
+
task_external_id=$(echo "$task_json" | jq -r '.external_id')
|
|
368
|
+
task_title=$(echo "$task_json" | jq -r '.title')
|
|
369
|
+
echo "[$name] Retrying task: $task_title (source=$task_source_type id=$task_external_id)"
|
|
370
|
+
# Proceed directly to Step 3 — skip source polling and the "After claim" section
|
|
371
|
+
fi
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
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.
|
|
375
|
+
|
|
376
|
+
**Update last_tick:**
|
|
377
|
+
```bash
|
|
378
|
+
tmp="${cfg}.tmp"
|
|
379
|
+
jq --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.last_tick = $ts' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Emit dashboard event:**
|
|
383
|
+
```bash
|
|
384
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
385
|
+
CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
|
|
386
|
+
SESSION_ID="monitor-${name}-$(date -u +%Y%m%dT%H%M%S)"
|
|
387
|
+
curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
|
|
388
|
+
-H "Content-Type: application/json" \
|
|
389
|
+
-d "$(jq -cn --arg sid "$SESSION_ID" --arg name "$name" \
|
|
390
|
+
'{type:"monitor:tick",session:$sid,monitor:$name,ts:(now*1000|floor)}')" || true
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Poll each source (in order, stop after first claimable task found):**
|
|
394
|
+
|
|
395
|
+
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:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
task_claimed=false
|
|
399
|
+
if [ -z "$retry_task" ]; then
|
|
400
|
+
task_json=""
|
|
401
|
+
while IFS= read -r src; do
|
|
402
|
+
src_type=$(echo "$src" | jq -r '.type')
|
|
403
|
+
case "$src_type" in
|
|
404
|
+
linear)
|
|
405
|
+
# === Linear adapter (below) ===
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
> 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.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
#### Source Adapter: Linear
|
|
413
|
+
|
|
414
|
+
> **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.
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# Extract filter fields from $src
|
|
418
|
+
_lin_team=$(echo "$src" | jq -r '.filter.team // ""')
|
|
419
|
+
_lin_assignee=$(echo "$src" | jq -r '.filter.assignees[0] // ""')
|
|
420
|
+
_lin_states=$(echo "$src" | jq -r '.filter.states // ["Todo"] | join(",")')
|
|
421
|
+
_lin_labels=$(echo "$src" | jq -c '.filter.labels // []')
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Use `mcp__claude_ai_Linear__list_issues` with:
|
|
425
|
+
- `teamId`: `$_lin_team`
|
|
426
|
+
- `assigneeId` / `assigneeEmail`: `$_lin_assignee` (if non-empty)
|
|
427
|
+
- State filter: resolved from `$_lin_states`
|
|
428
|
+
- Label filter applied client-side after fetch
|
|
429
|
+
|
|
430
|
+
The MCP call returns a JSON array. Bind it to a shell variable immediately after the call:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# issues = JSON array returned by mcp__claude_ai_Linear__list_issues
|
|
434
|
+
# Assign the raw JSON response to $issues before the loop below:
|
|
435
|
+
issues='[]' # Claude MUST overwrite this with the real array returned by mcp__claude_ai_Linear__list_issues before the loop runs
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Then iterate with an explicit bash loop (same pattern as Monotask/Filesystem adapters):
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
while IFS= read -r issue_json; do
|
|
442
|
+
[ -z "$issue_json" ] && continue
|
|
443
|
+
|
|
444
|
+
issue_id=$(echo "$issue_json" | jq -r '.id')
|
|
445
|
+
issue_title=$(echo "$issue_json" | jq -r '.title')
|
|
446
|
+
issue_desc=$(echo "$issue_json" | jq -r '.description // ""')
|
|
447
|
+
issue_url=$(echo "$issue_json" | jq -r '.url // ""')
|
|
448
|
+
issue_labels=$(echo "$issue_json" | jq -c '[(.labels // [])[] | .name]')
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
1. Check dedup — skip if already processed:
|
|
452
|
+
```bash
|
|
453
|
+
already_processed=$(jq -r --arg id "linear:${issue_id}" '.processed_ids[$id] // empty' "$state_file")
|
|
454
|
+
[ -n "$already_processed" ] && continue
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
2. Client-side label filter — skip if issue doesn't match configured labels:
|
|
458
|
+
```bash
|
|
459
|
+
if [ "$(echo "$_lin_labels" | jq 'length')" -gt 0 ]; then
|
|
460
|
+
has_label=$(jq -n --argjson want "$_lin_labels" --argjson got "$issue_labels" \
|
|
461
|
+
'($want - ($want - $got)) | length > 0')
|
|
462
|
+
[ "$has_label" != "true" ] && continue
|
|
463
|
+
fi
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
3. Build task object:
|
|
467
|
+
```bash
|
|
468
|
+
task_json=$(jq -cn \
|
|
469
|
+
--arg eid "$issue_id" \
|
|
470
|
+
--arg title "$issue_title" \
|
|
471
|
+
--arg desc "$issue_desc" \
|
|
472
|
+
--arg url "$issue_url" \
|
|
473
|
+
--argjson labels "$issue_labels" \
|
|
474
|
+
'{
|
|
475
|
+
"source_type": "linear",
|
|
476
|
+
"external_id": $eid,
|
|
477
|
+
"title": $title,
|
|
478
|
+
"description": $desc,
|
|
479
|
+
"url": $url,
|
|
480
|
+
"labels": $labels
|
|
481
|
+
}')
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Claim** (MCP — not bash): Call `mcp__claude_ai_Linear__save_issue` with:
|
|
485
|
+
- `issueId`: `$issue_id`
|
|
486
|
+
- Set state to "In Progress"
|
|
487
|
+
- Add label: `monitor:claimed`
|
|
488
|
+
|
|
489
|
+
**Progress comment** (MCP): Call `mcp__claude_ai_Linear__save_comment` with:
|
|
490
|
+
- `issueId`: `$issue_id`
|
|
491
|
+
- `body`: `"[Monitor: ${name}] Claimed by AI agent. Starting execution with agent type \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"`
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
task_claimed=true
|
|
495
|
+
break 2 # break 2: exit per-issue loop AND outer source loop
|
|
496
|
+
done < <(echo "$issues" | jq -c '.[]') # close per-issue loop
|
|
497
|
+
;; # end linear case branch
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
#### Source Adapter: GitHub Issues/PRs
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
# Extract filter fields from $src
|
|
506
|
+
_gh_repo=$(echo "$src" | jq -r '.filter.repo // ""')
|
|
507
|
+
_gh_assignee=$(echo "$src" | jq -r '.filter.assignee // ""')
|
|
508
|
+
_gh_state=$(echo "$src" | jq -r '.filter.state // "open"')
|
|
509
|
+
|
|
510
|
+
# Build --label flags as an array (gh issue list takes one --label per flag, not comma-separated)
|
|
511
|
+
gh_label_args=()
|
|
512
|
+
while IFS= read -r _lbl; do
|
|
513
|
+
[ -n "$_lbl" ] && gh_label_args+=(--label "$_lbl")
|
|
514
|
+
done < <(echo "$src" | jq -r '(.filter.labels // [])[]')
|
|
515
|
+
|
|
516
|
+
# Poll via gh CLI — capture into $issues
|
|
517
|
+
issues=$(gh issue list \
|
|
518
|
+
--repo "$_gh_repo" \
|
|
519
|
+
${_gh_assignee:+--assignee "$_gh_assignee"} \
|
|
520
|
+
"${gh_label_args[@]}" \
|
|
521
|
+
--state "$_gh_state" \
|
|
522
|
+
--json number,title,body,url,labels,assignees \
|
|
523
|
+
--limit 20)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Iterate with an explicit bash loop:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
while IFS= read -r issue_json; do
|
|
530
|
+
[ -z "$issue_json" ] && continue
|
|
531
|
+
|
|
532
|
+
number=$(echo "$issue_json" | jq -r '.number')
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
1. Check dedup — TWO checks required:
|
|
536
|
+
```bash
|
|
537
|
+
# Local dedup: skip if already in processed_ids
|
|
538
|
+
already_processed=$(jq -r --arg id "github:${number}" '.processed_ids[$id] // empty' "$state_file")
|
|
539
|
+
[ -n "$already_processed" ] && continue
|
|
540
|
+
|
|
541
|
+
# Remote dedup: skip if issue already has label monitor:claimed (externally labeled or other instance)
|
|
542
|
+
already_claimed=$(echo "$issue_json" | jq -r '[(.labels // [])[] | .name] | index("monitor:claimed")')
|
|
543
|
+
[ "$already_claimed" != "null" ] && continue
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
2. Build task object:
|
|
547
|
+
```bash
|
|
548
|
+
task_json=$(jq -cn \
|
|
549
|
+
--argjson issue "$issue_json" \
|
|
550
|
+
--arg repo "$_gh_repo" \
|
|
551
|
+
'{
|
|
552
|
+
"source_type": "github",
|
|
553
|
+
"external_id": ($issue.number | tostring),
|
|
554
|
+
"title": $issue.title,
|
|
555
|
+
"description": ($issue.body // ""),
|
|
556
|
+
"url": $issue.url,
|
|
557
|
+
"repo": $repo,
|
|
558
|
+
"labels": [($issue.labels // [])[] | .name]
|
|
559
|
+
}')
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**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:
|
|
563
|
+
```bash
|
|
564
|
+
# Run GitHub Label Bootstrap here (see "GitHub Label Bootstrap" section) — idempotent, guarded
|
|
565
|
+
gh issue edit "$number" --repo "$_gh_repo" --add-label "monitor:claimed,monitor:in-progress"
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Progress comment:**
|
|
569
|
+
```bash
|
|
570
|
+
gh issue comment "$number" --repo "$_gh_repo" --body \
|
|
571
|
+
"[Monitor: ${name}] Claimed by AI agent. Executing with \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
task_claimed=true
|
|
576
|
+
break 2 # break 2: exit per-issue loop AND outer source loop
|
|
577
|
+
done < <(echo "$issues" | jq -c '.[]') # close per-issue loop
|
|
578
|
+
;; # end github case branch
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Stage labels** (applied progressively throughout execution):
|
|
582
|
+
- `monitor:claimed` → task picked up
|
|
583
|
+
- `monitor:in-progress` → agent is executing
|
|
584
|
+
- `monitor:review` → agent completed, awaiting human review (optional)
|
|
585
|
+
- `monitor:done` → fully complete
|
|
586
|
+
- `monitor:failed` → all retries exhausted
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
#### Source Adapter: Monotask
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
# Extract filter fields from $src
|
|
594
|
+
_mt_board=$(echo "$src" | jq -r '.filter.board // ""')
|
|
595
|
+
_mt_col=$(echo "$src" | jq -r '.filter.column // "Todo"')
|
|
596
|
+
_mt_label=$(echo "$src" | jq -r '.filter.label // "role:ai-agent"')
|
|
597
|
+
|
|
598
|
+
# Resolve board_id from board title
|
|
599
|
+
board_id=$(monotask board list --json | jq -r --arg t "$_mt_board" '.[] | select(.title==$t) | .id' | head -1)
|
|
600
|
+
if [ -z "$board_id" ]; then
|
|
601
|
+
echo "[monotask] Board '$_mt_board' not found — skipping source."
|
|
602
|
+
# skip to next source adapter
|
|
603
|
+
else
|
|
604
|
+
|
|
605
|
+
# Resolve column ids
|
|
606
|
+
cols=$(monotask column list "$board_id" --json)
|
|
607
|
+
todo_col=$(echo "$cols" | jq -r --arg t "$_mt_col" '.[] | select(.title==$t) | .id' | head -1)
|
|
608
|
+
doing_col=$(echo "$cols" | jq -r '.[] | select(.title=="Doing" or .title=="In Progress") | .id' | head -1)
|
|
609
|
+
done_col=$(echo "$cols" | jq -r '.[] | select(.title=="Done") | .id' | head -1)
|
|
610
|
+
|
|
611
|
+
# Poll: unclaimed cards in todo column with matching label
|
|
612
|
+
cards=$(monotask card list "$board_id" --col "$todo_col" --label "$_mt_label" --json \
|
|
613
|
+
| jq '[.[] | select((.labels // []) | index("claimed") | not)]')
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Iterate over cards with an explicit bash loop (same pattern as the filesystem adapter):
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
while IFS= read -r card; do
|
|
620
|
+
[ -z "$card" ] && continue
|
|
621
|
+
|
|
622
|
+
card_id=$(echo "$card" | jq -r '.id')
|
|
623
|
+
card_title=$(echo "$card" | jq -r '.title')
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
1. Check dedup:
|
|
627
|
+
```bash
|
|
628
|
+
already_processed=$(jq -r --arg id "monotask:${card_id}" '.processed_ids[$id] // empty' "$state_file")
|
|
629
|
+
[ -n "$already_processed" ] && continue
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Build task object** (includes board/column IDs so Step 4 can re-extract them from `task_json` without relying on polling-scope shell vars):
|
|
633
|
+
```bash
|
|
634
|
+
task_json=$(jq -cn \
|
|
635
|
+
--arg eid "$card_id" \
|
|
636
|
+
--arg title "$card_title" \
|
|
637
|
+
--arg board "$board_id" \
|
|
638
|
+
--arg doing "$doing_col" \
|
|
639
|
+
--arg done "$done_col" \
|
|
640
|
+
'{
|
|
641
|
+
"source_type": "monotask",
|
|
642
|
+
"external_id": $eid,
|
|
643
|
+
"title": $title,
|
|
644
|
+
"description": "",
|
|
645
|
+
"board_id": $board,
|
|
646
|
+
"doing_col": $doing,
|
|
647
|
+
"done_col": $done
|
|
648
|
+
}')
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**Claim:**
|
|
652
|
+
```bash
|
|
653
|
+
monotask card move "$board_id" "$card_id" "$doing_col" --json
|
|
654
|
+
monotask card label add "$board_id" "$card_id" "claimed" --json
|
|
655
|
+
monotask card label add "$board_id" "$card_id" "monitor:in-progress" --json
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Progress comment:**
|
|
659
|
+
```bash
|
|
660
|
+
monotask card comment add "$board_id" "$card_id" \
|
|
661
|
+
"[Monitor: ${name}] Claimed by AI agent. Executing with \`${agent_type}\`. Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
662
|
+
|
|
663
|
+
task_claimed=true
|
|
664
|
+
break 2 # break 2: exit per-card loop AND outer source loop
|
|
665
|
+
done < <(echo "$cards" | jq -c '.[]') # close per-card loop
|
|
666
|
+
fi # close 'if [ -z "$board_id" ]' else block
|
|
667
|
+
;; # end monotask case branch
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
#### Source Adapter: Filesystem
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
# Extract filter fields from $src
|
|
676
|
+
folder=$(echo "$src" | jq -r '.filter.folder // "./tasks"')
|
|
677
|
+
|
|
678
|
+
if [ ! -d "$folder" ]; then
|
|
679
|
+
echo "[filesystem] Folder '$folder' not found — skipping source."
|
|
680
|
+
# skip to next source adapter
|
|
681
|
+
else
|
|
682
|
+
|
|
683
|
+
# Poll: find unclaimed .task files
|
|
684
|
+
tasks=$(find "$folder" -name "*.task" -not -name "*.claimed" -not -name "*.done" -not -name "*.failed" 2>/dev/null)
|
|
685
|
+
|
|
686
|
+
# Iterate — stop after first claimable file
|
|
687
|
+
while IFS= read -r task_file; do
|
|
688
|
+
[ -z "$task_file" ] && continue
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
(All per-file steps are inside this while loop; close with `done <<< "$tasks"` after the claim. Then close the `else` block with `fi`.)
|
|
692
|
+
|
|
693
|
+
For each `.task` file (`task_file` = absolute path):
|
|
694
|
+
1. Check dedup:
|
|
695
|
+
```bash
|
|
696
|
+
already_processed=$(jq -r --arg id "filesystem:${task_file}" '.processed_ids[$id] // empty' "$state_file")
|
|
697
|
+
[ -n "$already_processed" ] && continue
|
|
698
|
+
```
|
|
699
|
+
2. Read task content:
|
|
700
|
+
```bash
|
|
701
|
+
task_title=$(head -1 "$task_file")
|
|
702
|
+
task_description=$(tail -n +2 "$task_file")
|
|
703
|
+
task_base="${task_file%.task}"
|
|
704
|
+
task_json=$(jq -cn \
|
|
705
|
+
--arg title "$task_title" \
|
|
706
|
+
--arg description "$task_description" \
|
|
707
|
+
--arg eid "$task_file" \
|
|
708
|
+
--arg base_path "$task_base" \
|
|
709
|
+
'{
|
|
710
|
+
"source_type": "filesystem",
|
|
711
|
+
"external_id": $eid,
|
|
712
|
+
"title": $title,
|
|
713
|
+
"description": $description,
|
|
714
|
+
"base_path": $base_path
|
|
715
|
+
}')
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
**Claim:**
|
|
719
|
+
```bash
|
|
720
|
+
mv "${task_file}" "${task_base}.task.claimed"
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Progress log:**
|
|
724
|
+
```bash
|
|
725
|
+
echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — Claimed. Executing with ${agent_type}." \
|
|
726
|
+
>> "${task_base}.task.log"
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
task_claimed=true
|
|
731
|
+
break 2 # break 2: exit per-file loop AND outer source loop
|
|
732
|
+
done <<< "$tasks" # close per-file while loop (reached only if no file was claimed)
|
|
733
|
+
fi # close 'if [ ! -d "$folder" ]' else block
|
|
734
|
+
;; # end filesystem case branch
|
|
735
|
+
esac
|
|
736
|
+
done < <(jq -c '.sources[]' "$cfg")
|
|
737
|
+
fi # close 'if [ -z "$retry_task" ]' guard
|
|
738
|
+
# After: if $task_claimed=true, proceed to "After claim"; otherwise skip to Step 5
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
**Stage files:** `.task` → `.task.claimed` → `.task.done` or `.task.failed`
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
#### After a task is claimed (all sources — common code)
|
|
746
|
+
|
|
747
|
+
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.
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
if [ "$task_claimed" = "true" ]; then
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
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.
|
|
754
|
+
|
|
755
|
+
**Extract task metadata (must run first, before all blocks below):**
|
|
756
|
+
```bash
|
|
757
|
+
task_source_type=$(echo "$task_json" | jq -r '.source_type')
|
|
758
|
+
task_external_id=$(echo "$task_json" | jq -r '.external_id')
|
|
759
|
+
task_title=$(echo "$task_json" | jq -r '.title')
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Register in state as in-flight** (stores full task object for retry replay):
|
|
763
|
+
```bash
|
|
764
|
+
tmp="${state_file}.tmp"
|
|
765
|
+
jq --argjson task "$task_json" \
|
|
766
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
767
|
+
'.in_flight += [$task + {"claimed_at": $ts, "retry_count": 0}]' \
|
|
768
|
+
"$state_file" > "$tmp" && mv "$tmp" "$state_file"
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Mark as processed in dedup index:**
|
|
772
|
+
```bash
|
|
773
|
+
tmp="${state_file}.tmp"
|
|
774
|
+
jq --arg key "${task_source_type}:${task_external_id}" \
|
|
775
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
776
|
+
'.processed_ids[$key] = {status: "in_flight", claimed_at: $ts, retry_count: 0}' \
|
|
777
|
+
"$state_file" > "$tmp" && mv "$tmp" "$state_file"
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**Increment stats:**
|
|
781
|
+
```bash
|
|
782
|
+
tmp="${cfg}.tmp"
|
|
783
|
+
jq '.stats.total_claimed += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
784
|
+
fi # end 'if [ "$task_claimed" = "true" ]'
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Step 3 — Execute the Task
|
|
790
|
+
|
|
791
|
+
**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.
|
|
792
|
+
|
|
793
|
+
```bash
|
|
794
|
+
if [ -n "$task_json" ]; then
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
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.
|
|
798
|
+
|
|
799
|
+
The return value of the `Task` tool call is the agent's full text output. Capture it into `agent_output`:
|
|
800
|
+
|
|
801
|
+
```javascript
|
|
802
|
+
agent_output = Task({
|
|
803
|
+
subagent_type: agent_type, // from monitor config
|
|
804
|
+
description: `Monitor "${name}" executing: ${task.title}`,
|
|
805
|
+
run_in_background: false,
|
|
806
|
+
prompt: `You are an AI agent executing a task claimed by the Mastermind Monitor "${name}".
|
|
807
|
+
|
|
808
|
+
TASK: ${task.title}
|
|
809
|
+
|
|
810
|
+
DESCRIPTION:
|
|
811
|
+
${task.description || "(no description provided)"}
|
|
812
|
+
|
|
813
|
+
SOURCE: ${task.source_type} | ID: ${task.external_id}
|
|
814
|
+
URL: ${task.url || "n/a"}
|
|
815
|
+
|
|
816
|
+
INSTRUCTIONS:
|
|
817
|
+
1. Understand the task from the title and description above.
|
|
818
|
+
2. Execute the task fully and completely using available tools.
|
|
819
|
+
3. For code tasks: read relevant files, make changes, run tests if possible.
|
|
820
|
+
4. For research tasks: gather information, synthesize findings.
|
|
821
|
+
5. Produce a clear result summary at the end.
|
|
822
|
+
|
|
823
|
+
OUTPUT FORMAT:
|
|
824
|
+
At the end of your work, output a JSON block EXACTLY like this:
|
|
825
|
+
\`\`\`json
|
|
826
|
+
{
|
|
827
|
+
"status": "done|failed",
|
|
828
|
+
"summary": "one paragraph summary of what was accomplished",
|
|
829
|
+
"artifacts": ["list of files changed or created"],
|
|
830
|
+
"next_actions": ["any follow-up suggestions"]
|
|
831
|
+
}
|
|
832
|
+
\`\`\`
|
|
833
|
+
|
|
834
|
+
If you encounter an error you cannot recover from, set status to "failed" and explain in summary.`
|
|
835
|
+
})
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
`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:
|
|
839
|
+
|
|
840
|
+
```bash
|
|
841
|
+
# Task metadata (safe re-extract — already set by claim/retry path)
|
|
842
|
+
task_source_type=$(echo "$task_json" | jq -r '.source_type')
|
|
843
|
+
task_external_id=$(echo "$task_json" | jq -r '.external_id')
|
|
844
|
+
task_title=$(echo "$task_json" | jq -r '.title')
|
|
845
|
+
|
|
846
|
+
# Parse result JSON block from agent_output (all three fields in one python pass)
|
|
847
|
+
_result_json=$(echo "$agent_output" | python3 -c "
|
|
848
|
+
import sys, re, json
|
|
849
|
+
m = re.search(r'\`\`\`json\s*(\{.*?\})\s*\`\`\`', sys.stdin.read(), re.DOTALL)
|
|
850
|
+
print(m.group(1) if m else '{}')" 2>/dev/null || echo '{}')
|
|
851
|
+
|
|
852
|
+
result_status=$(echo "$_result_json" | jq -r '.status // "failed"')
|
|
853
|
+
result_summary=$(echo "$_result_json" | jq -r '.summary // "(no summary)"')
|
|
854
|
+
result_artifacts=$(echo "$_result_json" | jq -r '(.artifacts // []) | join(", ")')
|
|
855
|
+
result_next_actions=$(echo "$_result_json" | jq -r '(.next_actions // []) | join(", ")')
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## Step 4 — Handle Result
|
|
861
|
+
|
|
862
|
+
Branch on `result_status`:
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
if [ "$result_status" = "done" ]; then
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### On success (`status: "done"`)
|
|
869
|
+
|
|
870
|
+
**Post completion comment/update per source** — dispatch on `$task_source_type`:
|
|
871
|
+
|
|
872
|
+
**Linear** (MCP tool calls — not bash; load Linear MCP schema via ToolSearch first): for `task_source_type=linear`:
|
|
873
|
+
|
|
874
|
+
Call `mcp__claude_ai_Linear__save_comment` with:
|
|
875
|
+
- `issueId`: `$task_external_id`
|
|
876
|
+
- `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):
|
|
877
|
+
|
|
878
|
+
```
|
|
879
|
+
[Monitor: ${name}] ✓ Task complete.
|
|
880
|
+
|
|
881
|
+
Summary: ${result_summary}
|
|
882
|
+
|
|
883
|
+
Artifacts: ${result_artifacts}
|
|
884
|
+
Next actions: ${result_next_actions}
|
|
885
|
+
|
|
886
|
+
Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ) | Agent: ${agent_type}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Then call `mcp__claude_ai_Linear__save_issue` with:
|
|
890
|
+
- `issueId`: `$task_external_id`
|
|
891
|
+
- `stateId`: resolved ID for state name "Done" in this team
|
|
892
|
+
|
|
893
|
+
**GitHub, Monotask, Filesystem** — dispatched via `case`:
|
|
894
|
+
```bash
|
|
895
|
+
case "$task_source_type" in
|
|
896
|
+
github)
|
|
897
|
+
task_repo=$(echo "$task_json" | jq -r '.repo')
|
|
898
|
+
gh issue comment "$task_external_id" --repo "$task_repo" --body \
|
|
899
|
+
"[Monitor: ${name}] Task complete.
|
|
900
|
+
|
|
901
|
+
Summary: ${result_summary}
|
|
902
|
+
|
|
903
|
+
Artifacts: ${result_artifacts}
|
|
904
|
+
Next actions: ${result_next_actions}
|
|
905
|
+
Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
906
|
+
|
|
907
|
+
gh issue edit "$task_external_id" --repo "$task_repo" \
|
|
908
|
+
--remove-label "monitor:in-progress" \
|
|
909
|
+
--add-label "monitor:done"
|
|
910
|
+
|
|
911
|
+
# If it was a "complete this task" issue, close it:
|
|
912
|
+
# gh issue close "$task_external_id" --repo "$task_repo" --comment "Closed by monitor after completion."
|
|
913
|
+
;;
|
|
914
|
+
monotask)
|
|
915
|
+
# Re-extract from task_json — polling-scope vars are not reliable on retry path
|
|
916
|
+
board_id=$(echo "$task_json" | jq -r '.board_id')
|
|
917
|
+
card_id=$(echo "$task_json" | jq -r '.external_id')
|
|
918
|
+
done_col=$(echo "$task_json" | jq -r '.done_col')
|
|
919
|
+
|
|
920
|
+
monotask card comment add "$board_id" "$card_id" \
|
|
921
|
+
"[Monitor: ${name}] Task complete.
|
|
922
|
+
|
|
923
|
+
Summary: ${result_summary}
|
|
924
|
+
|
|
925
|
+
Artifacts: ${result_artifacts}
|
|
926
|
+
Next actions: ${result_next_actions}
|
|
927
|
+
Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
928
|
+
|
|
929
|
+
monotask card move "$board_id" "$card_id" "$done_col" --json
|
|
930
|
+
monotask card label remove "$board_id" "$card_id" "monitor:in-progress" --json
|
|
931
|
+
monotask card label add "$board_id" "$card_id" "monitor:done" --json
|
|
932
|
+
;;
|
|
933
|
+
filesystem)
|
|
934
|
+
task_base=$(echo "$task_json" | jq -r '.base_path')
|
|
935
|
+
echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — DONE." >> "${task_base}.task.log"
|
|
936
|
+
echo "Summary: ${result_summary}" >> "${task_base}.task.log"
|
|
937
|
+
echo "Artifacts: ${result_artifacts}" >> "${task_base}.task.log"
|
|
938
|
+
echo "Next actions: ${result_next_actions}" >> "${task_base}.task.log"
|
|
939
|
+
mv "${task_base}.task.claimed" "${task_base}.task.done"
|
|
940
|
+
;;
|
|
941
|
+
esac # end task_source_type dispatch (success path)
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Update state — remove from in-flight, mark done:**
|
|
945
|
+
```bash
|
|
946
|
+
tmp="${state_file}.tmp"
|
|
947
|
+
jq --arg key "${task_source_type}:${task_external_id}" \
|
|
948
|
+
--arg eid "$task_external_id" \
|
|
949
|
+
--arg stype "$task_source_type" \
|
|
950
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
951
|
+
'(.processed_ids[$key].status = "done") |
|
|
952
|
+
(.processed_ids[$key].done_at = $ts) |
|
|
953
|
+
(.in_flight = [.in_flight[] | select(.external_id != $eid or .source_type != $stype)])' \
|
|
954
|
+
"$state_file" > "$tmp" && mv "$tmp" "$state_file"
|
|
955
|
+
|
|
956
|
+
tmp="${cfg}.tmp"
|
|
957
|
+
jq '.stats.done += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**Emit dashboard event:**
|
|
961
|
+
```bash
|
|
962
|
+
curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
|
|
963
|
+
-H "Content-Type: application/json" \
|
|
964
|
+
-d "$(jq -cn --arg sid "$SESSION_ID" --arg name "$name" --arg title "$task_title" \
|
|
965
|
+
'{type:"monitor:task:done",session:$sid,monitor:$name,task:$title,ts:(now*1000|floor)}')" || true
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
```bash
|
|
969
|
+
else # result_status != "done"
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### On failure
|
|
973
|
+
|
|
974
|
+
**Retry logic (max 3 attempts):**
|
|
975
|
+
|
|
976
|
+
```bash
|
|
977
|
+
retry_count=$(jq -r --arg key "${task_source_type}:${task_external_id}" \
|
|
978
|
+
'.processed_ids[$key].retry_count // 0' "$state_file")
|
|
979
|
+
|
|
980
|
+
if [ "$retry_count" -lt 3 ]; then
|
|
981
|
+
new_retry=$((retry_count + 1))
|
|
982
|
+
echo "[$name] Task failed (attempt $new_retry/3). Will retry next tick."
|
|
983
|
+
|
|
984
|
+
# Update retry count in state
|
|
985
|
+
tmp="${state_file}.tmp"
|
|
986
|
+
jq --arg key "${task_source_type}:${task_external_id}" \
|
|
987
|
+
--arg eid "$task_external_id" \
|
|
988
|
+
--arg stype "$task_source_type" \
|
|
989
|
+
--argjson r "$new_retry" \
|
|
990
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
991
|
+
'(.processed_ids[$key].retry_count = $r) |
|
|
992
|
+
(.processed_ids[$key].last_failure = $ts) |
|
|
993
|
+
(.processed_ids[$key].status = "retry_pending") |
|
|
994
|
+
(.in_flight = [.in_flight[] |
|
|
995
|
+
if (.external_id == $eid and .source_type == $stype)
|
|
996
|
+
then .retry_count = $r | .last_failure = $ts
|
|
997
|
+
else . end])' \
|
|
998
|
+
"$state_file" > "$tmp" && mv "$tmp" "$state_file"
|
|
999
|
+
|
|
1000
|
+
tmp="${cfg}.tmp"
|
|
1001
|
+
jq '.stats.retried += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
1002
|
+
|
|
1003
|
+
# Post retry comment per source
|
|
1004
|
+
_retry_msg="[Monitor: ${name}] Task failed (attempt ${new_retry}/3). Will retry next tick. Error: ${result_summary}"
|
|
1005
|
+
# Linear (MCP — not bash): call mcp__claude_ai_Linear__save_comment with
|
|
1006
|
+
# issueId=$task_external_id body="$_retry_msg"
|
|
1007
|
+
# Then call mcp__claude_ai_Linear__save_issue to set state back to "In Progress".
|
|
1008
|
+
|
|
1009
|
+
case "$task_source_type" in
|
|
1010
|
+
github)
|
|
1011
|
+
task_repo=$(echo "$task_json" | jq -r '.repo')
|
|
1012
|
+
gh issue comment "$task_external_id" --repo "$task_repo" --body "$_retry_msg" 2>/dev/null || true
|
|
1013
|
+
;;
|
|
1014
|
+
monotask)
|
|
1015
|
+
_board=$(echo "$task_json" | jq -r '.board_id')
|
|
1016
|
+
monotask card comment add "$_board" "$task_external_id" "$_retry_msg" 2>/dev/null || true
|
|
1017
|
+
;;
|
|
1018
|
+
filesystem)
|
|
1019
|
+
_base=$(echo "$task_json" | jq -r '.base_path')
|
|
1020
|
+
echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — RETRY ${new_retry}/3. ${result_summary}" >> "${_base}.task.log"
|
|
1021
|
+
;;
|
|
1022
|
+
esac
|
|
1023
|
+
|
|
1024
|
+
else
|
|
1025
|
+
echo "[$name] Task failed after 3 attempts. Marking as failed."
|
|
1026
|
+
|
|
1027
|
+
# Post final failure comment and set external status per source
|
|
1028
|
+
_fail_msg="[Monitor: ${name}] Task failed after 3 attempts. Manual intervention required.
|
|
1029
|
+
Error: ${result_summary}
|
|
1030
|
+
Last attempt: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1031
|
+
|
|
1032
|
+
# Linear (MCP — not bash): call mcp__claude_ai_Linear__save_comment with
|
|
1033
|
+
# issueId=$task_external_id body="$_fail_msg"
|
|
1034
|
+
# Then call mcp__claude_ai_Linear__save_issue to:
|
|
1035
|
+
# - remove labels "monitor:claimed" and "monitor:in-progress"
|
|
1036
|
+
# - add label "monitor:failed"
|
|
1037
|
+
# - set state to "Cancelled" or "Blocked" as appropriate for the team.
|
|
1038
|
+
|
|
1039
|
+
case "$task_source_type" in
|
|
1040
|
+
github)
|
|
1041
|
+
task_repo=$(echo "$task_json" | jq -r '.repo')
|
|
1042
|
+
gh issue comment "$task_external_id" --repo "$task_repo" --body "$_fail_msg" 2>/dev/null || true
|
|
1043
|
+
gh issue edit "$task_external_id" --repo "$task_repo" \
|
|
1044
|
+
--add-label "monitor:failed" --remove-label "monitor:in-progress" 2>/dev/null || true
|
|
1045
|
+
;;
|
|
1046
|
+
monotask)
|
|
1047
|
+
_board=$(echo "$task_json" | jq -r '.board_id')
|
|
1048
|
+
monotask card comment add "$_board" "$task_external_id" "$_fail_msg" 2>/dev/null || true
|
|
1049
|
+
monotask card label remove "$_board" "$task_external_id" "monitor:in-progress" 2>/dev/null || true
|
|
1050
|
+
monotask card label add "$_board" "$task_external_id" "monitor:failed" 2>/dev/null || true
|
|
1051
|
+
;;
|
|
1052
|
+
filesystem)
|
|
1053
|
+
_base=$(echo "$task_json" | jq -r '.base_path')
|
|
1054
|
+
echo "[Monitor: ${name}] $(date -u +%Y-%m-%dT%H:%M:%SZ) — FAILED after 3 attempts. ${result_summary}" >> "${_base}.task.log"
|
|
1055
|
+
mv "${_base}.task.claimed" "${_base}.task.failed" 2>/dev/null || true
|
|
1056
|
+
;;
|
|
1057
|
+
esac
|
|
1058
|
+
|
|
1059
|
+
# Update state
|
|
1060
|
+
tmp="${state_file}.tmp"
|
|
1061
|
+
jq --arg key "${task_source_type}:${task_external_id}" \
|
|
1062
|
+
--arg eid "$task_external_id" \
|
|
1063
|
+
--arg stype "$task_source_type" \
|
|
1064
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
1065
|
+
'(.processed_ids[$key].status = "failed") |
|
|
1066
|
+
(.processed_ids[$key].failed_at = $ts) |
|
|
1067
|
+
(.in_flight = [.in_flight[] | select(.external_id != $eid or .source_type != $stype)])' \
|
|
1068
|
+
"$state_file" > "$tmp" && mv "$tmp" "$state_file"
|
|
1069
|
+
|
|
1070
|
+
tmp="${cfg}.tmp"
|
|
1071
|
+
jq '.stats.failed += 1' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
1072
|
+
fi # end permanent-fail branch (if retry_count -lt 3 ... else ... fi)
|
|
1073
|
+
fi # end result_status branch (if result_status = "done" ... else ... fi)
|
|
1074
|
+
|
|
1075
|
+
fi # end task guard (if [ -n "$task_json" ]; only runs Steps 3-4 when a task was claimed or retried)
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## Step 5 — Reschedule Next Tick
|
|
1081
|
+
|
|
1082
|
+
Always the last step, regardless of whether a task was found/executed.
|
|
1083
|
+
|
|
1084
|
+
Call `ScheduleWakeup` with:
|
|
1085
|
+
- `delaySeconds`: value of `poll_interval` from config (default 120)
|
|
1086
|
+
- `prompt`: `/mastermind:monitor --action tick --name <name>`
|
|
1087
|
+
- `reason`: `Monitor <name> polling every <poll_interval>s (<N sources>)`
|
|
1088
|
+
|
|
1089
|
+
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.
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## Step 6 — Return Output
|
|
1094
|
+
|
|
1095
|
+
```yaml
|
|
1096
|
+
domain: ops
|
|
1097
|
+
status: complete
|
|
1098
|
+
action: <action>
|
|
1099
|
+
monitor: <name>
|
|
1100
|
+
tasks_this_tick: <0 or 1>
|
|
1101
|
+
task_result: <done|failed|retry_pending|none>
|
|
1102
|
+
next_tick_in: <poll_interval>s
|
|
1103
|
+
state_file: .monomind/monitor/<name>-state.json
|
|
1104
|
+
run_id: <SESSION_ID>
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
1109
|
+
## Step 7 — Brain Write (standalone only)
|
|
1110
|
+
|
|
1111
|
+
If `caller` is not "command", follow `_protocol.md` Brain Write Procedure for domain `ops`.
|
|
1112
|
+
|
|
1113
|
+
---
|
|
1114
|
+
|
|
1115
|
+
## Configuration File Schemas
|
|
1116
|
+
|
|
1117
|
+
### `.monomind/monitor/<name>.json`
|
|
1118
|
+
|
|
1119
|
+
```json
|
|
1120
|
+
{
|
|
1121
|
+
"name": "dev-agent",
|
|
1122
|
+
"status": "active",
|
|
1123
|
+
"poll_interval": 120,
|
|
1124
|
+
"agent_type": "coder",
|
|
1125
|
+
"max_concurrent": 1,
|
|
1126
|
+
"sources": [
|
|
1127
|
+
{
|
|
1128
|
+
"type": "linear",
|
|
1129
|
+
"filter": {
|
|
1130
|
+
"team": "ENG",
|
|
1131
|
+
"assignees": ["morteza@agent-f.com"],
|
|
1132
|
+
"states": ["Todo"],
|
|
1133
|
+
"labels": ["ai-agent"],
|
|
1134
|
+
"project": ""
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"type": "github",
|
|
1139
|
+
"filter": {
|
|
1140
|
+
"repo": "nokhodian/monomind",
|
|
1141
|
+
"assignee": "nokhodian",
|
|
1142
|
+
"labels": ["ai-agent"],
|
|
1143
|
+
"state": "open",
|
|
1144
|
+
"type": "issue"
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
"type": "monotask",
|
|
1149
|
+
"filter": {
|
|
1150
|
+
"board": "monomind-tasks-dev",
|
|
1151
|
+
"column": "Todo",
|
|
1152
|
+
"label": "role:ai-agent"
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
"type": "filesystem",
|
|
1157
|
+
"filter": {
|
|
1158
|
+
"folder": "./tasks",
|
|
1159
|
+
"glob": "*.task",
|
|
1160
|
+
"user": ""
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
],
|
|
1164
|
+
"stats": {
|
|
1165
|
+
"done": 0,
|
|
1166
|
+
"failed": 0,
|
|
1167
|
+
"retried": 0,
|
|
1168
|
+
"total_claimed": 0
|
|
1169
|
+
},
|
|
1170
|
+
"created_at": "2026-05-18T00:00:00Z",
|
|
1171
|
+
"last_tick": null
|
|
1172
|
+
}
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### `.monomind/monitor/<name>-state.json`
|
|
1176
|
+
|
|
1177
|
+
```json
|
|
1178
|
+
{
|
|
1179
|
+
"processed_ids": {
|
|
1180
|
+
"linear:LIN-123": {
|
|
1181
|
+
"status": "done",
|
|
1182
|
+
"claimed_at": "2026-05-18T10:00:00Z",
|
|
1183
|
+
"done_at": "2026-05-18T10:05:00Z",
|
|
1184
|
+
"retry_count": 0
|
|
1185
|
+
},
|
|
1186
|
+
"github:456": {
|
|
1187
|
+
"status": "failed",
|
|
1188
|
+
"claimed_at": "2026-05-18T09:00:00Z",
|
|
1189
|
+
"failed_at": "2026-05-18T09:20:00Z",
|
|
1190
|
+
"retry_count": 3
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
"in_flight": [
|
|
1194
|
+
{
|
|
1195
|
+
"source_type": "monotask",
|
|
1196
|
+
"external_id": "abc-uuid",
|
|
1197
|
+
"title": "Fix auth bug",
|
|
1198
|
+
"description": "",
|
|
1199
|
+
"board_id": "board-uuid-here",
|
|
1200
|
+
"doing_col": "doing-col-uuid",
|
|
1201
|
+
"done_col": "done-col-uuid",
|
|
1202
|
+
"claimed_at": "2026-05-18T10:10:00Z",
|
|
1203
|
+
"retry_count": 1,
|
|
1204
|
+
"last_failure": "2026-05-18T10:12:00Z"
|
|
1205
|
+
}
|
|
1206
|
+
]
|
|
1207
|
+
}
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
## Quick Reference — Common Commands
|
|
1213
|
+
|
|
1214
|
+
```bash
|
|
1215
|
+
# Create a monitor watching GitHub issues assigned to you
|
|
1216
|
+
/mastermind:monitor --action start --name dev-watcher \
|
|
1217
|
+
--source github --project nokhodian/monomind --user nokhodian \
|
|
1218
|
+
--label ai-agent --agent coder --interval 120
|
|
1219
|
+
|
|
1220
|
+
# Add a Linear source to an existing monitor
|
|
1221
|
+
/mastermind:monitor --action add-source --name dev-watcher \
|
|
1222
|
+
--source linear --team ENG --user morteza@agent-f.com \
|
|
1223
|
+
--state Todo --label ai-agent
|
|
1224
|
+
|
|
1225
|
+
# Add a monotask board source
|
|
1226
|
+
/mastermind:monitor --action add-source --name dev-watcher \
|
|
1227
|
+
--source monotask --project monomind-tasks-dev \
|
|
1228
|
+
--state Todo --label role:ai-agent
|
|
1229
|
+
|
|
1230
|
+
# Add a filesystem folder source
|
|
1231
|
+
/mastermind:monitor --action add-source --name dev-watcher \
|
|
1232
|
+
--source filesystem --folder ./tasks
|
|
1233
|
+
|
|
1234
|
+
# Check status
|
|
1235
|
+
/mastermind:monitor --action status --name dev-watcher
|
|
1236
|
+
|
|
1237
|
+
# List all monitors
|
|
1238
|
+
/mastermind:monitor
|
|
1239
|
+
|
|
1240
|
+
# Pause (stops auto-rescheduling after current tick)
|
|
1241
|
+
/mastermind:monitor --action pause --name dev-watcher
|
|
1242
|
+
|
|
1243
|
+
# Resume (re-enters the tick loop)
|
|
1244
|
+
/mastermind:monitor --action resume --name dev-watcher
|
|
1245
|
+
|
|
1246
|
+
# Stop permanently
|
|
1247
|
+
/mastermind:monitor --action stop --name dev-watcher
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
---
|
|
1251
|
+
|
|
1252
|
+
## GitHub Label Bootstrap
|
|
1253
|
+
|
|
1254
|
+
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:
|
|
1255
|
+
|
|
1256
|
+
```bash
|
|
1257
|
+
# Bootstrap monitor labels on every claim — --force makes each call idempotent (no-ops if label already exists)
|
|
1258
|
+
# Running unconditionally is safe: 5 lightweight API calls, prevents gaps if any label was deleted externally
|
|
1259
|
+
for label in "monitor:claimed" "monitor:in-progress" "monitor:review" "monitor:done" "monitor:failed"; do
|
|
1260
|
+
# shasum is available on both macOS and Linux; md5sum is Linux-only
|
|
1261
|
+
color=$(printf '%s' "$label" | shasum | cut -c1-6)
|
|
1262
|
+
gh label create "$label" --repo "$_gh_repo" --color "$color" --force 2>/dev/null || true
|
|
1263
|
+
done
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Important Behavioral Notes
|
|
1269
|
+
|
|
1270
|
+
**`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.
|
|
1271
|
+
|
|
1272
|
+
**`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).
|
|
1273
|
+
|
|
1274
|
+
**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.
|
|
1275
|
+
|
|
1276
|
+
---
|
|
1277
|
+
|
|
1278
|
+
## Notes on Filter Precedence
|
|
1279
|
+
|
|
1280
|
+
When multiple users or states are specified (via repeated `--user` or `--state` flags), the monitor ORs them:
|
|
1281
|
+
- `--user alice --user bob` → fetch tasks assigned to alice OR bob
|
|
1282
|
+
- `--state Todo --state "In Progress"` → fetch tasks in either state
|
|
1283
|
+
|
|
1284
|
+
The `--label` flag is ANDed with user/state: only tasks matching the label AND the user/state filter are claimed.
|