@monoes/monomindcli 1.9.16 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.claude/commands/mastermind/_repeat.md +182 -39
  2. package/.claude/commands/mastermind/architect.md +17 -11
  3. package/.claude/commands/mastermind/brain.md +4 -0
  4. package/.claude/commands/mastermind/build.md +4 -0
  5. package/.claude/commands/mastermind/content.md +4 -0
  6. package/.claude/commands/mastermind/createorg.md +5 -3
  7. package/.claude/commands/mastermind/finance.md +4 -0
  8. package/.claude/commands/mastermind/idea.md +4 -0
  9. package/.claude/commands/mastermind/marketing.md +4 -0
  10. package/.claude/commands/mastermind/master.md +100 -46
  11. package/.claude/commands/mastermind/ops.md +4 -0
  12. package/.claude/commands/mastermind/release.md +4 -0
  13. package/.claude/commands/mastermind/research.md +4 -0
  14. package/.claude/commands/mastermind/review.md +4 -0
  15. package/.claude/commands/mastermind/runorg.md +5 -3
  16. package/.claude/commands/mastermind/sales.md +4 -0
  17. package/.claude/commands/mastermind/techport.md +9 -0
  18. package/.claude/commands/monomind/do.md +5 -1
  19. package/.claude/commands/monomind/idea.md +5 -1
  20. package/.claude/commands/monomind/improve.md +5 -1
  21. package/.claude/commands/monomind/repeat.md +85 -29
  22. package/.claude/commands/monomind/review.md +6 -2
  23. package/.claude/commands/monomind/understand.md +10 -8
  24. package/.claude/helpers/extras-registry.json +235 -235
  25. package/.claude/helpers/graphify-freshen.cjs +13 -1
  26. package/.claude/helpers/hook-handler.cjs +1 -1
  27. package/.claude/helpers/router.cjs +4 -1
  28. package/.claude/skills/mastermind/_protocol.md +37 -21
  29. package/.claude/skills/mastermind/access.md +236 -0
  30. package/.claude/skills/mastermind/activity.md +191 -0
  31. package/.claude/skills/mastermind/adapter-manager.md +259 -0
  32. package/.claude/skills/mastermind/adapters.md +204 -0
  33. package/.claude/skills/mastermind/agent-detail.md +242 -0
  34. package/.claude/skills/mastermind/agents.md +178 -0
  35. package/.claude/skills/mastermind/approval-detail.md +259 -0
  36. package/.claude/skills/mastermind/approve.md +181 -0
  37. package/.claude/skills/mastermind/architect.md +24 -8
  38. package/.claude/skills/mastermind/backup.md +197 -0
  39. package/.claude/skills/mastermind/bootstrap.md +190 -0
  40. package/.claude/skills/mastermind/budgets.md +237 -0
  41. package/.claude/skills/mastermind/companies.md +256 -0
  42. package/.claude/skills/mastermind/costs.md +151 -0
  43. package/.claude/skills/mastermind/createorg.md +23 -5
  44. package/.claude/skills/mastermind/diagnose.md +249 -0
  45. package/.claude/skills/mastermind/env.md +198 -0
  46. package/.claude/skills/mastermind/environments.md +250 -0
  47. package/.claude/skills/mastermind/export.md +324 -0
  48. package/.claude/skills/mastermind/goal-detail.md +255 -0
  49. package/.claude/skills/mastermind/goals.md +149 -0
  50. package/.claude/skills/mastermind/heartbeat.md +164 -0
  51. package/.claude/skills/mastermind/idea.md +318 -186
  52. package/.claude/skills/mastermind/import.md +281 -0
  53. package/.claude/skills/mastermind/inbox.md +214 -0
  54. package/.claude/skills/mastermind/instance-settings.md +315 -0
  55. package/.claude/skills/mastermind/instance.md +231 -0
  56. package/.claude/skills/mastermind/invite-landing.md +227 -0
  57. package/.claude/skills/mastermind/invites.md +254 -0
  58. package/.claude/skills/mastermind/issue-detail.md +291 -0
  59. package/.claude/skills/mastermind/issues.md +235 -0
  60. package/.claude/skills/mastermind/join-queue.md +170 -0
  61. package/.claude/skills/mastermind/liveness.md +392 -0
  62. package/.claude/skills/mastermind/memory.md +321 -0
  63. package/.claude/skills/mastermind/my-issues.md +146 -0
  64. package/.claude/skills/mastermind/new-agent.md +241 -0
  65. package/.claude/skills/mastermind/org-chart.md +207 -0
  66. package/.claude/skills/mastermind/org-settings.md +217 -0
  67. package/.claude/skills/mastermind/plan-to-tasks.md +136 -0
  68. package/.claude/skills/mastermind/plugin-manager.md +241 -0
  69. package/.claude/skills/mastermind/plugin-settings.md +273 -0
  70. package/.claude/skills/mastermind/plugins.md +190 -0
  71. package/.claude/skills/mastermind/profile.md +187 -0
  72. package/.claude/skills/mastermind/project-detail.md +249 -0
  73. package/.claude/skills/mastermind/project-workspace.md +244 -0
  74. package/.claude/skills/mastermind/projects.md +164 -0
  75. package/.claude/skills/mastermind/routine-detail.md +253 -0
  76. package/.claude/skills/mastermind/routines.md +202 -0
  77. package/.claude/skills/mastermind/runorg.md +74 -9
  78. package/.claude/skills/mastermind/search.md +186 -0
  79. package/.claude/skills/mastermind/secrets.md +199 -0
  80. package/.claude/skills/mastermind/skills.md +156 -0
  81. package/.claude/skills/mastermind/tasks.md +149 -0
  82. package/.claude/skills/mastermind/techport.md +5 -5
  83. package/.claude/skills/mastermind/threads.md +259 -0
  84. package/.claude/skills/mastermind/tree-control.md +250 -0
  85. package/.claude/skills/mastermind/wiki.md +314 -0
  86. package/.claude/skills/mastermind/workspace-detail.md +317 -0
  87. package/.claude/skills/mastermind/workspaces.md +261 -0
  88. package/.claude/skills/mastermind/worktree.md +187 -0
  89. package/dist/src/init/executor.js +8 -8
  90. package/dist/src/init/executor.js.map +1 -1
  91. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  92. package/dist/src/init/statusline-generator.js +12 -0
  93. package/dist/src/init/statusline-generator.js.map +1 -1
  94. package/dist/src/ui/.monomind/data/ranked-context.json +1 -1
  95. package/dist/src/ui/.monomind/loops/mastermind-review-1778664132789.json +16 -0
  96. package/dist/src/ui/.monomind/sessions/current.json +5 -5
  97. package/dist/src/ui/.monomind/sessions/session-1776778451399.json +15 -0
  98. package/dist/src/ui/dashboard.html +3030 -181
  99. package/dist/src/ui/data/mastermind-events.jsonl +8 -0
  100. package/dist/src/ui/data/mastermind-sessions.json +1 -0
  101. package/dist/src/ui/server.mjs +738 -0
  102. package/dist/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +1 -1
  104. package/.claude/skills/.monomind/data/ranked-context.json +0 -5
  105. package/.claude/skills/.monomind/sessions/current.json +0 -13
  106. package/.claude/skills/.monomind/sessions/session-1777829336455.json +0 -15
  107. package/.claude/skills/.monomind/sessions/session-1777831614725.json +0 -15
  108. package/.claude/skills/.monomind/sessions/session-1777832095857.json +0 -15
  109. package/.claude/skills/.monomind/sessions/session-1777839814183.json +0 -15
  110. package/.claude/skills/.monomind/sessions/session-1777841847131.json +0 -15
  111. package/.claude/skills/.monomind/sessions/session-1777843309463.json +0 -15
  112. package/.claude/skills/.monomind/sessions/session-1777880867159.json +0 -15
  113. package/.claude/skills/.monomind/sessions/session-1777881884593.json +0 -15
  114. package/.claude/skills/.monomind/sessions/session-1777884090471.json +0 -15
  115. package/.claude/skills/.monomind/sessions/session-1777884808221.json +0 -15
  116. package/.claude/skills/.monomind/sessions/session-1777885672155.json +0 -15
  117. package/.claude/skills/.monomind/sessions/session-1777886852818.json +0 -15
  118. package/.claude/skills/.monomind/sessions/session-1777896532690.json +0 -15
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: mastermind-threads
3
+ description: Mastermind threads — list, view, and create conversation threads within an org. Threads are human-or-agent discussions attached to issues, goals, or the org itself. Reads from -threads.jsonl org state files.
4
+ type: domain-skill
5
+ default_mode: auto
6
+ ---
7
+
8
+ # Mastermind Threads
9
+
10
+ This skill is invoked by `mastermind:threads` or directly via `/mastermind:threads`.
11
+
12
+ ---
13
+
14
+ ## Inputs
15
+
16
+ - `brain_context`: BRAIN CONTEXT block (injected by command, or loaded below if standalone)
17
+ - `org_name`: org to query (required)
18
+ - `action`: list | view | create | reply
19
+ - `thread_id`: thread ID (required for view/reply)
20
+ - `issue_id`: filter by issue ID (optional)
21
+ - `message`: message body (required for create/reply)
22
+ - `author`: author name or agent ID (default: current user)
23
+ - `limit`: max threads to show (default: 20)
24
+ - `caller`: command | master
25
+
26
+ ---
27
+
28
+ ## Step 0 — Brain Load (standalone only)
29
+
30
+ If `caller` is not "command", load brain context following _protocol.md Brain Load Procedure with namespace: `ops`.
31
+
32
+ ---
33
+
34
+ ## Step 1 — Load Threads File
35
+
36
+ ```bash
37
+ orgFile=".monomind/orgs/${org_name}.json"
38
+ [ ! -f "$orgFile" ] && { echo "ERROR: Org '${org_name}' not found."; exit 1; }
39
+
40
+ threadsFile=".monomind/orgs/${org_name}-threads.jsonl"
41
+ limit="${limit:-20}"
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Step 2 — Execute Action
47
+
48
+ ### list (default)
49
+
50
+ ```bash
51
+ echo "THREADS — $org_name"
52
+ echo "────────────────────────────────────────────────────────"
53
+
54
+ if [ ! -f "$threadsFile" ] || [ ! -s "$threadsFile" ]; then
55
+ echo " No threads found."
56
+ echo ""
57
+ echo " Create: /mastermind:threads --org $org_name --action create --message 'Hello team'"
58
+ exit 0
59
+ fi
60
+
61
+ python3 - "$threadsFile" "${issue_id:-}" "$limit" <<'PYEOF'
62
+ import json, sys
63
+
64
+ path = sys.argv[1]
65
+ issue_f = sys.argv[2]
66
+ limit = int(sys.argv[3])
67
+
68
+ threads = []
69
+ with open(path) as f:
70
+ for line in f:
71
+ line = line.strip()
72
+ if not line: continue
73
+ try:
74
+ t = json.loads(line)
75
+ if t.get("type","thread") == "thread":
76
+ threads.append(t)
77
+ except: pass
78
+
79
+ if issue_f:
80
+ threads = [t for t in threads if t.get("issueId") == issue_f]
81
+
82
+ threads = threads[-limit:]
83
+
84
+ if not threads:
85
+ print(f" (no threads{' filtered by issue=' + issue_f if issue_f else ''})")
86
+ else:
87
+ print(f" {'ID':<28} {'SUBJECT':<32} {'AUTHOR':<20} {'MSGS':<6} CREATED")
88
+ print(" " + "─" * 96)
89
+ for t in threads:
90
+ tid = t.get("id","?")[:28]
91
+ subj = (t.get("subject") or "(no subject)")[:32]
92
+ author = (t.get("authorName") or t.get("authorId") or "—")[:20]
93
+ msgs = len(t.get("messages", []))
94
+ created = (t.get("createdAt") or "-")[:10]
95
+ print(f" {tid:<28} {subj:<32} {author:<20} {msgs:<6} {created}")
96
+
97
+ print(f"\n {len(threads)} thread(s). View: /mastermind:threads --org <org> --action view --thread-id <id>")
98
+ PYEOF
99
+ ```
100
+
101
+ ### view
102
+
103
+ ```bash
104
+ [ -z "$thread_id" ] && { echo "ERROR: --thread-id required."; exit 1; }
105
+
106
+ if [ ! -f "$threadsFile" ]; then
107
+ echo "No threads found for org '$org_name'."
108
+ exit 0
109
+ fi
110
+
111
+ python3 - "$threadsFile" "$thread_id" <<'PYEOF'
112
+ import json, sys
113
+
114
+ path, tid = sys.argv[1], sys.argv[2]
115
+
116
+ thread = None
117
+ with open(path) as f:
118
+ for line in f:
119
+ line = line.strip()
120
+ if not line: continue
121
+ try:
122
+ t = json.loads(line)
123
+ if t.get("id") == tid and t.get("type","thread") == "thread":
124
+ thread = t
125
+ except: pass
126
+
127
+ if not thread:
128
+ print(f"ERROR: Thread '{tid}' not found.")
129
+ sys.exit(1)
130
+
131
+ subj = thread.get("subject","(no subject)")
132
+ issue = thread.get("issueId","")
133
+ created = thread.get("createdAt","-")
134
+
135
+ print(f"THREAD: {subj}")
136
+ print(f" ID: {tid}")
137
+ if issue: print(f" Issue: {issue}")
138
+ print(f" Created: {created}")
139
+ print()
140
+ print("MESSAGES")
141
+ print("────────────────────────────────────────────────────────")
142
+
143
+ msgs = thread.get("messages", [])
144
+ if not msgs:
145
+ print(" (no messages)")
146
+ else:
147
+ for m in msgs:
148
+ author = m.get("authorName") or m.get("authorId") or "?"
149
+ ts = (m.get("createdAt") or "?")[:16].replace("T"," ")
150
+ body = m.get("body","")
151
+ print(f" [{ts}] {author}:")
152
+ for line in body.split("\n"):
153
+ print(f" {line}")
154
+ print()
155
+ PYEOF
156
+ ```
157
+
158
+ ### create
159
+
160
+ ```bash
161
+ [ -z "$message" ] && { echo "ERROR: --message required."; exit 1; }
162
+
163
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
164
+ newId="thread-$(python3 -c 'import time; print(int(time.time()*1000))')"
165
+ authorVal="${author:-operator}"
166
+
167
+ python3 - "$threadsFile" "$newId" "${title:-}" "$message" "$authorVal" "${issue_id:-}" "$ts" <<'PYEOF'
168
+ import json, sys
169
+
170
+ path, tid, subj, body, author, issue, ts = sys.argv[1:]
171
+ thread = {
172
+ "id": tid,
173
+ "type": "thread",
174
+ "subject": subj or "(no subject)",
175
+ "issueId": issue or None,
176
+ "authorId": author,
177
+ "authorName": author,
178
+ "createdAt": ts,
179
+ "messages": [{
180
+ "id": f"msg-{tid}",
181
+ "authorId": author,
182
+ "authorName": author,
183
+ "body": body,
184
+ "createdAt": ts,
185
+ }]
186
+ }
187
+ with open(path, "a") as f:
188
+ f.write(json.dumps(thread) + "\n")
189
+ print(f" Created thread: {tid}")
190
+ print(f" Subject: {subj or '(no subject)'}")
191
+ print(f" Message: {body[:80]}{'...' if len(body) > 80 else ''}")
192
+ PYEOF
193
+ ```
194
+
195
+ ### reply
196
+
197
+ ```bash
198
+ [ -z "$thread_id" ] && { echo "ERROR: --thread-id required."; exit 1; }
199
+ [ -z "$message" ] && { echo "ERROR: --message required."; exit 1; }
200
+
201
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
202
+ authorVal="${author:-operator}"
203
+
204
+ # Read all threads, append message to matching thread, rewrite file
205
+ python3 - "$threadsFile" "$thread_id" "$message" "$authorVal" "$ts" <<'PYEOF'
206
+ import json, sys
207
+
208
+ path, tid, body, author, ts = sys.argv[1:]
209
+ threads = []
210
+ found = False
211
+ with open(path) as f:
212
+ for line in f:
213
+ line = line.strip()
214
+ if not line: continue
215
+ try:
216
+ t = json.loads(line)
217
+ if t.get("id") == tid:
218
+ msg_id = f"msg-{tid}-{len(t.get('messages',[]))}"
219
+ t.setdefault("messages",[]).append({
220
+ "id": msg_id,
221
+ "authorId": author,
222
+ "authorName": author,
223
+ "body": body,
224
+ "createdAt": ts,
225
+ })
226
+ found = True
227
+ threads.append(t)
228
+ except: pass
229
+
230
+ if not found:
231
+ print(f"ERROR: Thread '{tid}' not found.")
232
+ sys.exit(1)
233
+
234
+ with open(path, "w") as f:
235
+ for t in threads:
236
+ f.write(json.dumps(t) + "\n")
237
+
238
+ print(f" Reply added to thread: {tid}")
239
+ print(f" Author: {author} | {ts}")
240
+ PYEOF
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Step 3 — Return Output
246
+
247
+ ```yaml
248
+ domain: ops
249
+ status: complete
250
+ action: <action>
251
+ org_name: <org_name>
252
+ thread_id: <thread_id or new_id>
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Step 4 — Brain Write (standalone only)
258
+
259
+ If `caller` is not "command", follow _protocol.md Brain Write Procedure for domain `ops`.
@@ -0,0 +1,250 @@
1
+ ---
2
+ name: mastermind-tree-control
3
+ description: Mastermind tree-control — pause, hold, release, or preview recovery for an issue/task tree in an org. Lets board members stop runaway loops, hold trees during review, and resume work when ready. Mirrors Paperclip's issue-tree-control API.
4
+ type: domain-skill
5
+ default_mode: confirm
6
+ ---
7
+
8
+ # Mastermind Tree Control
9
+
10
+ This skill is invoked by `mastermind:tree-control` or directly via `/mastermind:tree-control`.
11
+
12
+ ---
13
+
14
+ ## Inputs
15
+
16
+ - `brain_context`: BRAIN CONTEXT block (injected by command, or loaded below if standalone)
17
+ - `org_name`: org to operate on (required)
18
+ - `issue_id`: root issue/task ID to control (required)
19
+ - `action`: preview | hold | release | cancel
20
+ - `reason`: reason for the hold/cancel (required for hold/cancel)
21
+ - `caller`: command | master
22
+
23
+ ---
24
+
25
+ ## Three Invariants (MUST be preserved)
26
+
27
+ All tree-control actions must respect these invariants:
28
+
29
+ 1. **Productive work continues.** Only hold what genuinely needs to stop; don't block productive branches.
30
+ 2. **Only real blockers stop work.** A hold is a deliberate decision, not a pseudo-stop.
31
+ 3. **No infinite loops.** Release must be possible — never hold without a clear release path.
32
+
33
+ ---
34
+
35
+ ## Step 0 — Brain Load (standalone only)
36
+
37
+ If `caller` is not "command", load brain context following _protocol.md Brain Load Procedure with namespace: `ops`.
38
+
39
+ ---
40
+
41
+ ## Step 1 — Load Org and Issue
42
+
43
+ ```bash
44
+ orgFile=".monomind/orgs/${org_name}.json"
45
+ [ ! -f "$orgFile" ] && { echo "ERROR: Org '${org_name}' not found."; exit 1; }
46
+ [ -z "$issue_id" ] && { echo "ERROR: --issue-id required."; exit 1; }
47
+
48
+ issuesFile=".monomind/orgs/${org_name}-issues.json"
49
+ [ ! -f "$issuesFile" ] && { echo "ERROR: No issues file found for org '${org_name}'."; exit 1; }
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Step 2 — Execute Action
55
+
56
+ ### preview (default)
57
+
58
+ Preview what a hold/cancel would affect without making changes:
59
+
60
+ ```bash
61
+ echo "TREE CONTROL PREVIEW — $org_name root: $issue_id"
62
+ echo "════════════════════════════════════════════════════════"
63
+
64
+ python3 - "$issuesFile" "$issue_id" <<'PYEOF'
65
+ import json, sys
66
+
67
+ data = json.load(open(sys.argv[1]))
68
+ rootId = sys.argv[2]
69
+ issues = {i["id"]: i for i in data.get("issues", [])}
70
+
71
+ root = issues.get(rootId)
72
+ if not root:
73
+ print(f"ERROR: Issue '{rootId}' not found.")
74
+ sys.exit(1)
75
+
76
+ print(f" Root: {root.get('id')} — {root.get('title','?')} [{root.get('status','?')}]")
77
+ print()
78
+
79
+ # Find subtree
80
+ def subtree(iid, depth=0):
81
+ children = [i for i in issues.values() if i.get("parentId") == iid or iid in (i.get("blockedByIssueIds") or [])]
82
+ for c in children:
83
+ st = c.get("status","?")
84
+ icon = "⚠" if st in ("in_progress","in_review") else "○"
85
+ print(f" {' ' * depth}{icon} {c['id']} — {c.get('title','?')} [{st}]")
86
+ subtree(c["id"], depth + 1)
87
+
88
+ subtree(rootId)
89
+
90
+ active = [i for i in issues.values()
91
+ if i.get("status") in ("in_progress","in_review")
92
+ and (i.get("parentId") == rootId or rootId in (i.get("blockedByIssueIds") or []))]
93
+ print()
94
+ print(f" Active issues in subtree: {len(active)}")
95
+ print(f" Holding root would affect all {len(active)} active issue(s).")
96
+ print()
97
+ print(" To hold: /mastermind:tree-control --org <org> --action hold --issue-id <id> --reason 'Review needed'")
98
+ print(" To cancel: /mastermind:tree-control --org <org> --action cancel --issue-id <id> --reason 'No longer needed'")
99
+ PYEOF
100
+ ```
101
+
102
+ ### hold
103
+
104
+ Place a hold on the issue tree (pauses all active work in the subtree):
105
+
106
+ ```bash
107
+ [ -z "$reason" ] && { echo "ERROR: --reason required for hold action."; exit 1; }
108
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
109
+
110
+ python3 - "$issuesFile" "$issue_id" "$reason" "$ts" <<'PYEOF'
111
+ import json, sys
112
+
113
+ path, rootId, reason, ts = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
114
+ data = json.load(open(path))
115
+ issues = data.get("issues", [])
116
+
117
+ # Add hold metadata to root issue
118
+ held = 0
119
+ for iss in issues:
120
+ if iss.get("id") == rootId:
121
+ iss.setdefault("holds", []).append({
122
+ "id": f"hold-{int(ts.replace('-','').replace(':','').replace('T','').replace('Z',''))[:14]}",
123
+ "reason": reason,
124
+ "createdAt": ts,
125
+ "status": "active"
126
+ })
127
+ held += 1
128
+ break
129
+
130
+ if not held:
131
+ print(f"ERROR: Issue '{rootId}' not found.")
132
+ sys.exit(1)
133
+
134
+ data["issues"] = issues
135
+ with open(path, "w") as f:
136
+ json.dump(data, f, indent=2)
137
+
138
+ print(f" HOLD PLACED on issue tree rooted at: {rootId}")
139
+ print(f" Reason: {reason}")
140
+ print(f" Applied: {ts}")
141
+ print()
142
+ print(" Active agents should detect the hold and pause execution.")
143
+ print(f" To release: /mastermind:tree-control --org <org> --action release --issue-id {rootId}")
144
+ PYEOF
145
+ ```
146
+
147
+ ### release
148
+
149
+ Release a hold from the issue tree:
150
+
151
+ ```bash
152
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
153
+
154
+ python3 - "$issuesFile" "$issue_id" "$ts" <<'PYEOF'
155
+ import json, sys
156
+
157
+ path, rootId, ts = sys.argv[1], sys.argv[2], sys.argv[3]
158
+ data = json.load(open(path))
159
+ issues = data.get("issues", [])
160
+
161
+ released = 0
162
+ for iss in issues:
163
+ if iss.get("id") == rootId:
164
+ holds = iss.get("holds", [])
165
+ active_holds = [h for h in holds if h.get("status") == "active"]
166
+ for h in active_holds:
167
+ h["status"] = "released"
168
+ h["releasedAt"] = ts
169
+ released += 1
170
+ iss["holds"] = holds
171
+ break
172
+
173
+ if not released:
174
+ print(f" No active holds found on issue '{rootId}'.")
175
+ print(" (Issue may already be running or was never held.)")
176
+ else:
177
+ data["issues"] = issues
178
+ with open(path, "w") as f:
179
+ json.dump(data, f, indent=2)
180
+ print(f" HOLD RELEASED on {rootId} — {released} hold(s) cleared.")
181
+ print(f" Released: {ts}")
182
+ print()
183
+ print(" Agents can now resume work on this issue tree.")
184
+ PYEOF
185
+ ```
186
+
187
+ ### cancel
188
+
189
+ Cancel an issue and its entire subtree:
190
+
191
+ ```bash
192
+ [ -z "$reason" ] && { echo "ERROR: --reason required for cancel action."; exit 1; }
193
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
194
+
195
+ echo "WARNING: This will mark '$issue_id' and its subtree as cancelled."
196
+ echo "Reason: $reason"
197
+ echo ""
198
+
199
+ python3 - "$issuesFile" "$issue_id" "$reason" "$ts" <<'PYEOF'
200
+ import json, sys
201
+
202
+ path, rootId, reason, ts = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
203
+ data = json.load(open(path))
204
+ issues = data.get("issues", [])
205
+ issues_map = {i["id"]: i for i in issues}
206
+
207
+ def cancel_subtree(iid):
208
+ iss = issues_map.get(iid)
209
+ if not iss:
210
+ return 0
211
+ if iss.get("status") in ("done","cancelled"):
212
+ return 0
213
+ iss["status"] = "cancelled"
214
+ iss["cancelledAt"] = ts
215
+ iss["cancelReason"] = reason
216
+ count = 1
217
+ children = [i for i in issues if i.get("parentId") == iid]
218
+ for c in children:
219
+ count += cancel_subtree(c["id"])
220
+ return count
221
+
222
+ cancelled = cancel_subtree(rootId)
223
+
224
+ data["issues"] = issues
225
+ with open(path, "w") as f:
226
+ json.dump(data, f, indent=2)
227
+
228
+ print(f" CANCELLED: {cancelled} issue(s) in tree rooted at {rootId}")
229
+ print(f" Reason: {reason}")
230
+ print(f" Applied: {ts}")
231
+ PYEOF
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Step 3 — Return Output
237
+
238
+ ```yaml
239
+ domain: ops
240
+ status: complete
241
+ action: <action>
242
+ org_name: <org_name>
243
+ issue_id: <issue_id>
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Step 4 — Brain Write (standalone only)
249
+
250
+ If `caller` is not "command", follow _protocol.md Brain Write Procedure for domain `ops`.