monomind 2.1.3 → 2.1.5

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 (27) hide show
  1. package/README.md +18 -17
  2. package/package.json +1 -1
  3. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
  4. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +32 -15
  5. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +238 -23
  6. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +74 -1
  7. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +14 -12
  8. package/packages/@monomind/cli/.claude/helpers/router.cjs +12 -2
  9. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +91 -701
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
  12. package/packages/@monomind/cli/README.md +18 -17
  13. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +18 -0
  14. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +76 -1
  15. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +7 -3
  16. package/packages/@monomind/cli/dist/src/orgrt/server.js +24 -0
  17. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +1 -0
  18. package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -0
  19. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +1 -1
  20. package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
  21. package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
  22. package/packages/@monomind/cli/dist/src/ui/dashboard.html +270 -499
  23. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +1 -1
  24. package/packages/@monomind/cli/dist/src/ui/server.mjs +132 -158
  25. package/packages/@monomind/cli/package.json +3 -3
  26. package/packages/@monomind/cli/scripts/publish.sh +6 -0
  27. package/scripts/generate-agent-avatars.mjs +3 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mastermind-org-settings
3
- description: Mastermind org-settings — edit org configuration (name, goal, topology, governance, budget), export org as portable JSON, and import an org from a previously exported file.
3
+ description: Mastermind org-settings — edit Org Runtime v2 configuration (name, goal, schedule, budget_tokens, memory_namespace, max_turns_per_message), export org as portable JSON, and import an org from a previously exported file.
4
4
  type: domain-skill
5
5
  default_mode: confirm
6
6
  ---
@@ -9,6 +9,8 @@ default_mode: confirm
9
9
 
10
10
  This skill is invoked by `mastermind:org-settings` or directly via `/mastermind:org-settings`.
11
11
 
12
+ It edits `.monomind/orgs/<org_name>.json` in place, and only ever touches fields that `OrgDefSchema` (`packages/@monomind/cli/src/orgrt/types.ts`) actually defines — every field listed below is read by the daemon (`org.ts`/`daemon.ts`/`session.ts`) at `monomind org run`/`serve` time. There is no `topology`, `governance`, `alert_threshold`, or `ceo_adapter` in Org Runtime v2 — those were v1 board/prompt-orchestration fields with no runtime effect and have been removed from this skill.
13
+
12
14
  ---
13
15
 
14
16
  ## Inputs
@@ -16,7 +18,7 @@ This skill is invoked by `mastermind:org-settings` or directly via `/mastermind:
16
18
  - `brain_context`: BRAIN CONTEXT block (injected by command, or loaded below if standalone)
17
19
  - `org_name`: org to configure (required)
18
20
  - `action`: show | edit | export | import
19
- - `field`: field to edit (name, goal, topology, governance, budget_tokens, alert_threshold, ceo_adapter)
21
+ - `field`: field to edit (name, goal, schedule, budget_tokens, memory_namespace, max_turns_per_message)
20
22
  - `value`: new value for the field
21
23
  - `export_path`: path to write exported JSON (default: `.monomind/exports/<org_name>-<timestamp>.json`)
22
24
  - `import_path`: path to JSON file to import from
@@ -49,19 +51,18 @@ Display current org config in a readable format:
49
51
  echo "ORG SETTINGS — ${org_name}"
50
52
  echo "──────────────────────────────────────────"
51
53
  jq -r '
52
- " Name: \(.name // .org_name // "-")",
53
- " Goal: \(.goal // "-")",
54
- " Topology: \(.topology // "hierarchical")",
55
- " Max Agents: \(.max_agents // 8)",
56
- " Governance: \(.governance.policy // "auto")",
57
- " Budget: \(.run_config.budget_tokens // 0) tokens",
58
- " Alert at: \((.run_config.alert_threshold // 0.8) * 100 | floor)%",
59
- " CEO Adapter: \(.run_config.ceo_adapter // "claude-sonnet-4-6")",
60
- " Roles: \(.roles | length) agents"
54
+ " Name: \(.name // "-")",
55
+ " Goal: \(.goal // "-")",
56
+ " Status: \(.status // "stopped")",
57
+ " Schedule: \(.schedule // "manual (no auto-schedule)")",
58
+ " Budget: \(.run_config.budget_tokens // 1000000) tokens",
59
+ " Memory namespace: \(.run_config.memory_namespace // ("org:" + (.name // "-")))",
60
+ " Max turns/message: \(.run_config.max_turns_per_message // 30)",
61
+ " Roles: \(.roles | length) agents"
61
62
  ' "$orgFile"
62
63
  echo ""
63
64
  echo " Run /mastermind:org-settings --org ${org_name} --action edit --field <field> --value <value>"
64
- echo " Fields: name | goal | topology | governance | budget_tokens | alert_threshold | ceo_adapter"
65
+ echo " Fields: name | goal | schedule | budget_tokens | memory_namespace | max_turns_per_message"
65
66
  ```
66
67
 
67
68
  ### edit
@@ -79,23 +80,30 @@ case "$field" in
79
80
  echo "$value" | grep -qE '^[a-z0-9][a-z0-9-]{0,63}$' || { echo "ERROR: name must match ^[a-z0-9][a-z0-9-]{0,63}$"; exit 1; }
80
81
  newOrgFile=".monomind/orgs/${value}.json"
81
82
  [ -f "$newOrgFile" ] && { echo "ERROR: An org named '${value}' already exists."; exit 1; }
83
+ # Refuse to rename while the daemon has this org running — it holds the old
84
+ # name's paths (defPath, cwd, runtime.json) in memory; a rename mid-run would
85
+ # orphan that live state instead of moving it.
86
+ runtime_status=$(jq -r '.status // "stopped"' ".monomind/orgs/${org_name}/runtime.json" 2>/dev/null || echo "stopped")
87
+ [ "$runtime_status" = "running" ] && { echo "ERROR: org '${org_name}' is running — stop it first: monomind org stop ${org_name}"; exit 1; }
82
88
  # Update the name field inside the JSON
83
89
  jq --arg v "$value" '.name = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
84
90
  # Rename the main config file
85
91
  mv "$orgFile" "$newOrgFile"
86
- # Rename all side-car data files
87
- for suffix in -state -goals -routines -approvals -activity -issues -members -projects -workspaces -worktrees -environments -plugins -adapters -threads -budgets -project-workspaces -approval-comments -bootstrap -secrets; do
92
+ # Rename all side-car artifact files — same suffix list as ORG_ARTIFACT_SUFFIXES
93
+ # in packages/@monomind/cli/src/commands/org.ts (single source of truth for what
94
+ # counts as an org-internal artifact vs. the org's own config file).
95
+ for suffix in -state -goals -threads -activity -approvals -members -secrets -budgets \
96
+ -routines -issues -projects -workspaces -worktrees -environments \
97
+ -plugins -adapters -join-requests -bootstrap -project-workspaces \
98
+ -approval-comments -skills; do
88
99
  old_file=".monomind/orgs/${org_name}${suffix}.json"
89
100
  [ -f "$old_file" ] && mv "$old_file" ".monomind/orgs/${value}${suffix}.json" || true
90
101
  old_jsonl=".monomind/orgs/${org_name}${suffix}.jsonl"
91
102
  [ -f "$old_jsonl" ] && mv "$old_jsonl" ".monomind/orgs/${value}${suffix}.jsonl" || true
92
103
  done
93
- # Rename loop prompt file if present (scheduled orgs)
94
- old_loop=".monomind/loops/${org_name}.md"
95
- [ -f "$old_loop" ] && mv "$old_loop" ".monomind/loops/${value}.md" || true
96
- # Rename stop file if present
97
- old_stop=".monomind/orgs/.stops/${org_name}.stop"
98
- [ -f "$old_stop" ] && mv "$old_stop" ".monomind/orgs/.stops/${value}.stop" || true
104
+ # Rename the org's runtime subdirectory (runs/, workspace/, stop file) if present
105
+ old_dir=".monomind/orgs/${org_name}"
106
+ [ -d "$old_dir" ] && mv "$old_dir" ".monomind/orgs/${value}" || true
99
107
  echo "Renamed org '${org_name}' → '${value}'"
100
108
  org_name="$value"
101
109
  orgFile="$newOrgFile"
@@ -104,42 +112,33 @@ case "$field" in
104
112
  jq --arg v "$value" '.goal = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
105
113
  echo "Updated: goal → $value"
106
114
  ;;
107
- topology)
108
- case "$value" in hierarchical|mesh|hierarchical-mesh|adaptive|star|ring) : ;; *)
109
- echo "ERROR: topology must be one of: hierarchical, mesh, hierarchical-mesh, adaptive, star, ring"; exit 1 ;;
110
- esac
111
- jq --arg v "$value" '.topology = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
112
- echo "Updated: topology → $value"
113
- ;;
114
- governance)
115
- case "$value" in auto|board|strict) : ;; *)
116
- echo "ERROR: governance must be one of: auto, board, strict"; exit 1 ;;
117
- esac
118
- jq --arg v "$value" '.governance.policy = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
119
- echo "Updated: governance → $value"
115
+ schedule)
116
+ # daemon format (parseSchedule in orgrt/scheduler.ts): "<N>s" | "<N>m" | "<N>h", or "none"/"" to clear
117
+ if [ "$value" = "none" ] || [ -z "$value" ]; then
118
+ jq '.schedule = null' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
119
+ echo "Updated: schedule none (manual run with monomind org run ${org_name})"
120
+ else
121
+ echo "$value" | grep -qE '^[0-9]+(s|m|h)$' || { echo "ERROR: schedule must match ^[0-9]+(s|m|h)$ (e.g. 30m, 2h) or 'none'"; exit 1; }
122
+ jq --arg v "$value" '.schedule = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
123
+ echo "Updated: schedule → $value (pick up with: monomind org serve)"
124
+ fi
120
125
  ;;
121
126
  budget_tokens)
122
127
  [[ "$value" =~ ^[0-9]+$ ]] || { echo "ERROR: budget_tokens must be a positive integer"; exit 1; }
123
128
  jq --argjson v "$value" '.run_config.budget_tokens = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
124
129
  echo "Updated: budget_tokens → $value"
125
130
  ;;
126
- alert_threshold)
127
- # accept 0.0–1.0 or 0–100
128
- if [[ "$value" =~ ^[0-9]+$ ]] && [ "$value" -gt 1 ]; then
129
- value=$(awk "BEGIN{printf \"%.2f\",$value/100}")
130
- fi
131
- jq --argjson v "$value" '.run_config.alert_threshold = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
132
- echo "Updated: alert_threshold → $value"
131
+ memory_namespace)
132
+ jq --arg v "$value" '.run_config.memory_namespace = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
133
+ echo "Updated: memory_namespace $value"
133
134
  ;;
134
- ceo_adapter)
135
- case "$value" in claude-sonnet-4-6|claude-opus-4-7|claude-haiku-4-5) : ;; *)
136
- echo "ERROR: ceo_adapter must be one of: claude-sonnet-4-6, claude-opus-4-7, claude-haiku-4-5"; exit 1 ;;
137
- esac
138
- jq --arg v "$value" '.run_config.ceo_adapter = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
139
- echo "Updated: ceo_adapter → $value"
135
+ max_turns_per_message)
136
+ [[ "$value" =~ ^[0-9]+$ ]] || { echo "ERROR: max_turns_per_message must be a positive integer"; exit 1; }
137
+ jq --argjson v "$value" '.run_config.max_turns_per_message = $v' "$orgFile" > "$tmp" && mv "$tmp" "$orgFile"
138
+ echo "Updated: max_turns_per_message → $value"
140
139
  ;;
141
140
  *)
142
- echo "ERROR: Unknown field '$field'. Valid fields: name, goal, topology, governance, budget_tokens, alert_threshold, ceo_adapter"
141
+ echo "ERROR: Unknown field '$field'. Valid fields: name, goal, schedule, budget_tokens, memory_namespace, max_turns_per_message"
143
142
  exit 1
144
143
  ;;
145
144
  esac
@@ -1,12 +1,15 @@
1
1
  ---
2
2
  name: mastermind-runorg
3
- description: Mastermind runorg — load a saved org definition and start it as a persistent autonomous agent organization. The boss agent coordinates all roles, agents pick up tasks from a shared board, and the org runs until stopped.
3
+ description: "DEPRECATED (2026-07): superseded by `monomind org run <name>` (SDK Org Runtime v2). Mastermind runorg — load a saved org definition and start it as a persistent, prompt-orchestrated agent organization. The boss agent coordinates all roles, agents pick up tasks from a shared board, and the org runs until stopped."
4
4
  type: domain-skill
5
5
  default_mode: auto
6
6
  ---
7
7
 
8
8
  # Mastermind Run Org
9
9
 
10
+ > **DEPRECATED (2026-07): superseded by `monomind org run <name>` (SDK Org Runtime v2).**
11
+ > This prompt-orchestrated path (Task-tool-spawned boss, monotask board, `ORG_VARS`/`runcontext.json`-style variable threading, manual dashboard-event curl calls) has no delivery guarantees and no ground-truth event stream — the v2 daemon spawns every role as its own live SDK session, routes messages directly via the `org_send` tool, and forwards dashboard events itself. It remains only for orgs not yet migrated off the v1 `topology`/`communication`/`board_id` config shape. New orgs created with `/mastermind:createorg` are already v2-shaped and MUST be started with `monomind org run <name>` — do not route them through this skill.
12
+
10
13
  This skill is invoked by `mastermind:runorg` or directly via `/mastermind:runorg`.
11
14
 
12
15
  ---
@@ -40,6 +43,14 @@ fi
40
43
  ```
41
44
  Return `status: blocked` with message: "Org '<org_name>' not found. Run /mastermind:createorg to define it."
42
45
 
46
+ **v2 config guard — check this before anything else.** `/mastermind:createorg` now writes v2-shaped configs with no `topology`/`board_id`/`communication` field at all:
47
+ ```bash
48
+ is_v2=$(jq -r 'if has("topology") or has("board_id") then "no" else "yes" end' ".monomind/orgs/${org_name}.json")
49
+ ```
50
+ If `is_v2 == "yes"`, **stop and return `status: blocked`** with message: "Org '<org_name>' is a v2 config (Org Runtime v2) — this skill only understands the deprecated v1 board/topology shape. Start it with: `monomind org run <org_name>` (or `monomind org serve` if it has a `schedule`)." Do not attempt to derive a topology, spawn a boss via Task tool, or touch a monotask board for a v2 config — every field this skill reads from Step 2 onward (`topology`, `board_id`, `todo_col_id`, `communication`) is simply absent there.
51
+
52
+ Only proceed past this guard for a genuine legacy v1 config (has `topology` and/or `board_id`).
53
+
43
54
  Validate the config has at minimum: `name`, `goal`, `roles` (non-empty array). The `communication` field is optional; fall back to `edges` if absent (both have the same schema: `from`, `to`, `type`).
44
55
 
45
56
  ---
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/banner.png" alt="Monomind" width="600" />
2
+ <img src="https://raw.githubusercontent.com/monoes/monomind/main/assets/banner.png" alt="Monomind" width="600" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Monomind</h1>
@@ -38,9 +38,9 @@ Monomind is an **open-source CLI and MCP server** that plugs into Claude Code vi
38
38
  - **Reusable slash commands** — 30+ development workflows (build, review, debug, TDD, architecture) available as `/mastermind:*` commands inside Claude Code.
39
39
 
40
40
  ```bash
41
- npm install -g monomind # MIT licensed, 0 external API calls
41
+ npm install -g monomind # MIT licensed, runs entirely on your machine
42
42
  cd your-project && monomind init
43
- claude mcp add monomind npx monomind mcp start
43
+ claude mcp add monomind -- npx -y monomind@latest mcp start
44
44
  ```
45
45
 
46
46
  ### Trust & Security
@@ -48,8 +48,8 @@ claude mcp add monomind npx monomind mcp start
48
48
  | Concern | Answer |
49
49
  |---|---|
50
50
  | **License** | [MIT](LICENSE) — use it however you want |
51
- | **Data privacy** | Everything runs locally. No telemetry, no phone-home, no external API calls from Monomind itself. Your code stays on your machine. |
52
- | **Dependencies** | Standard npm packages (tree-sitter, sql.js, zod). No native binaries. No post-install scripts that download code. |
51
+ | **Data privacy** | Everything runs locally — your code and memory store never leave your machine. The one exception: crash reporting is on by default (`monomind crash-reporting disable` to opt out) — a hard crash in monomind/mono-agent/monotask/mono-clip files a GitHub issue on that tool's own repo via the GitHub API. That's the only network call Monomind itself makes; it never phones a monoes-controlled server. |
52
+ | **Dependencies** | Standard npm packages (tree-sitter, better-sqlite3, sql.js, zod). tree-sitter and better-sqlite3 are native (prebuilt) Node addons, not pure JS/WASM — sql.js is WASM. No post-install scripts that download code. |
53
53
  | **Permissions** | Registers as an MCP server — Claude Code controls what tools are available and prompts you before executing anything sensitive. |
54
54
  | **Source** | Fully open. Read every line at [github.com/monoes/monomind](https://github.com/monoes/monomind). |
55
55
  | **Maintenance** | Active development, regular releases on npm. |
@@ -62,7 +62,7 @@ claude mcp add monomind npx monomind mcp start
62
62
 
63
63
  ### The idea
64
64
 
65
- Every business function needs a team. Define the org once as a JSON file — goal, roles, who reports to whom, per-role tool/file/budget policy — then run it as a real background daemon backed by the Claude Agent SDK. It persists across sessions, serves a live WebSocket dashboard, and can discover and message other Monomind orgs running on the same machine.
65
+ Every business function needs a team. Define the org once as a JSON file — goal, roles, who reports to whom, per-role tool/file/budget policy — then run it as a real background daemon backed by the Claude Agent SDK. It persists across sessions, streams live into the dashboard Claude Code auto-starts for the project, and can discover and message other Monomind orgs running on the same machine.
66
66
 
67
67
  ```mermaid
68
68
  flowchart TD
@@ -73,14 +73,14 @@ flowchart TD
73
73
  W["Writer"]
74
74
  S["SEO Specialist"]
75
75
  R["Reviewer"]
76
- DASH[("Live Dashboard\n:4243")]
76
+ DASH[("Dashboard\n:4242\nauto-started by\nClaude Code hook")]
77
77
  XORG[("Other orgs\ncross-process")]
78
78
 
79
79
  U --> DEF --> RUN --> BOSS
80
80
  BOSS -->|spawns| W
81
81
  BOSS -->|spawns| S
82
82
  BOSS -->|spawns| R
83
- RUN <-->|serves| DASH
83
+ RUN -->|forwards events| DASH
84
84
  RUN <-.->|--cross-process| XORG
85
85
 
86
86
  style BOSS fill:#00D2AA22,stroke:#00D2AA
@@ -93,13 +93,14 @@ flowchart TD
93
93
  ```bash
94
94
  # .monomind/orgs/<name>.json defines the org: goal, roles, policy.
95
95
  # See .monomind/orgs/sample-team.json in a fresh `monomind init` for a working example.
96
+ # Open the project in Claude Code first — a SessionStart hook auto-launches
97
+ # the dashboard at http://localhost:4242 if it isn't already running.
96
98
 
97
99
  monomind org run content-team --task "Build and publish 3 blog posts per week"
98
100
 
99
- # ✓ Live dashboard: http://localhost:4243
100
101
  # ✓ Boss agent (Claude Agent SDK session) spawns, reads the org goal,
101
102
  # assigns work to role agents, coordinates until the task completes
102
- # or you stop it.
103
+ # or you stop it. Every event streams into the dashboard above.
103
104
 
104
105
  monomind org status content-team # runtime state
105
106
  monomind org stop content-team # request a graceful stop
@@ -112,18 +113,18 @@ monomind org list # every org + status
112
113
  |---|---|
113
114
  | **OrgDaemon** | Hosts one or more orgs in a single process; real Claude Agent SDK sessions per role, not simulated |
114
115
  | **PolicyEngine** | Per-role gates on tool access, file read/write scope, web access, token budget — enforced, with a full audit trail |
115
- | **Live dashboard** | WebSocket-served at `:4243` by default (`--port` to change, `--serve=false` to disable) |
116
+ | **Dashboard** | `org run` forwards every event to the control server on `:4242` (found via `.monomind/control.json`) that server is auto-launched by a Claude Code SessionStart hook, not by any CLI command; there's no separate per-org dashboard process |
116
117
  | **Cross-process comms** | `--cross-process` (default on) lets orgs on different `monomind` processes/projects discover and message each other |
117
118
  | **Scheduling** | `monomind org serve` hosts orgs whose definition has a `schedule` field, running them on interval |
118
119
 
119
120
  ### Org management commands
120
121
 
121
122
  ```bash
122
- monomind org run <name> [--task "..."] [--port N] [--cross-process] # start a daemon
123
+ monomind org run <name> [--task "..."] [--cross-process] # start a daemon
123
124
  monomind org stop <name> # request a running org to stop
124
125
  monomind org status [name] # runtime state for one or all orgs
125
126
  monomind org list # list every org + status
126
- monomind org serve [--port N] # host-only mode, runs scheduled orgs
127
+ monomind org serve [--cross-process] # host-only mode, runs scheduled orgs
127
128
  monomind org delete <name> # remove an org
128
129
  ```
129
130
 
@@ -179,13 +180,13 @@ cd your-project
179
180
  monomind init
180
181
 
181
182
  # 3. Wire into Claude Code as an MCP server
182
- claude mcp add monomind npx monomind mcp start
183
+ claude mcp add monomind -- npx -y monomind@latest mcp start
183
184
 
184
185
  # 4. Health check
185
186
  monomind doctor --fix
186
187
  ```
187
188
 
188
- Open Claude Code. You now have 60+ slash commands available:
189
+ Open Claude Code. You now have 49 `/mastermind:*` workflows available:
189
190
 
190
191
  ```bash
191
192
  /mastermind:autodev --tillend # start autonomous code loop
@@ -275,7 +276,7 @@ const result = await fence.detect(userInput);
275
276
 
276
277
  ---
277
278
 
278
- ## 📋 60+ Slash Commands
279
+ ## 📋 49 Mastermind Commands
279
280
 
280
281
  Everything runs from inside Claude Code via slash commands. Here's the highlight reel:
281
282
 
@@ -308,7 +309,7 @@ Everything runs from inside Claude Code via slash commands. Here's the highlight
308
309
  | `/mastermind:finance` | Budgets, invoicing, modeling |
309
310
  | `/mastermind:ops` | Operations and workflow automation |
310
311
 
311
- **[→ Full reference (60+ commands)](https://monoes.github.io/monomind/#slash)**
312
+ **[→ Full reference (49 commands)](https://monoes.github.io/monomind/#slash)**
312
313
 
313
314
  ---
314
315
 
@@ -76,6 +76,24 @@ export declare class OrgDaemon {
76
76
  error: string;
77
77
  };
78
78
  private hasOrgDef;
79
+ private questionsPath;
80
+ private readQuestions;
81
+ private writeQuestions;
82
+ /** Agent-initiated human question (ask_human tool). Persists to questions.json (survives
83
+ * process/dashboard restarts) and emits a 'question' BusEvent so the dashboard's SSE
84
+ * stream and global inbox pick it up in real time. Returns a receipt string for the tool call. */
85
+ askHuman(org: string, role: string, question: string): Promise<string>;
86
+ /** Delivers a human's answer to a pending ask_human question. If the org is still
87
+ * running, pushes straight into the role's live mailbox (picked up on its very next
88
+ * generator tick — see Mailbox.stream()). If the org has since stopped, queues the
89
+ * answer via the same offline fallback deliver()/receiveRemote() already use
90
+ * (inbox.ts + autoWake) and it's delivered when the org next starts. */
91
+ answerQuestion(org: string, role: string, questionId: string, answer: string): Promise<{
92
+ ok: true;
93
+ } | {
94
+ ok: false;
95
+ error: string;
96
+ }>;
79
97
  /** Start an offline org in the background so queued messages get drained.
80
98
  * Fire-and-forget — errors are logged but don't propagate to the sender. */
81
99
  private autoWake;
@@ -1,6 +1,6 @@
1
1
  // packages/@monomind/cli/src/orgrt/daemon.ts
2
2
  // monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
3
- import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { readFileSync, mkdirSync, writeFileSync, existsSync, renameSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { OrgBus } from './bus.js';
6
6
  import { PolicyEngine } from './policy.js';
@@ -70,6 +70,7 @@ export class OrgDaemon {
70
70
  org: name, role, bus, policy, mailbox, cwd, def,
71
71
  maxTurns: def.run_config.max_turns_per_message,
72
72
  deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
73
+ askHuman: (role, question) => this.askHuman(name, role, question),
73
74
  queryFn: this.opts.queryFn,
74
75
  }).then(() => { runtime.status = 'ended'; })
75
76
  .catch((err) => {
@@ -223,6 +224,80 @@ export class OrgDaemon {
223
224
  hasOrgDef(name) {
224
225
  return existsSync(join(this.root, ORG_DIR, `${name}.json`));
225
226
  }
227
+ questionsPath(org) {
228
+ return join(this.root, ORG_DIR, org, 'questions.json');
229
+ }
230
+ readQuestions(org) {
231
+ try {
232
+ return JSON.parse(readFileSync(this.questionsPath(org), 'utf8'));
233
+ }
234
+ catch {
235
+ return { questions: [] };
236
+ }
237
+ }
238
+ writeQuestions(org, data) {
239
+ const dest = this.questionsPath(org);
240
+ mkdirSync(join(this.root, ORG_DIR, org), { recursive: true });
241
+ const tmp = `${dest}.${process.pid}.tmp`;
242
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
243
+ renameSync(tmp, dest);
244
+ }
245
+ /** Agent-initiated human question (ask_human tool). Persists to questions.json (survives
246
+ * process/dashboard restarts) and emits a 'question' BusEvent so the dashboard's SSE
247
+ * stream and global inbox pick it up in real time. Returns a receipt string for the tool call. */
248
+ async askHuman(org, role, question) {
249
+ const running = this.orgs.get(org);
250
+ const questionId = `q-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
251
+ const data = this.readQuestions(org);
252
+ data.questions.push({ questionId, role, question, ts: Date.now(), answer: null, answeredAt: null });
253
+ this.writeQuestions(org, data);
254
+ running?.bus.emit({ type: 'question', from: role, data: { questionId, question } });
255
+ return `Question recorded (id ${questionId}) — a human will answer it; you'll receive the answer as a new message.`;
256
+ }
257
+ /** Delivers a human's answer to a pending ask_human question. If the org is still
258
+ * running, pushes straight into the role's live mailbox (picked up on its very next
259
+ * generator tick — see Mailbox.stream()). If the org has since stopped, queues the
260
+ * answer via the same offline fallback deliver()/receiveRemote() already use
261
+ * (inbox.ts + autoWake) and it's delivered when the org next starts. */
262
+ async answerQuestion(org, role, questionId, answer) {
263
+ const data = this.readQuestions(org);
264
+ const idx = data.questions.findIndex(q => q.questionId === questionId);
265
+ if (idx === -1)
266
+ return { ok: false, error: `question "${questionId}" not found for org "${org}"` };
267
+ if (data.questions[idx].answer !== null)
268
+ return { ok: false, error: `question "${questionId}" already answered` };
269
+ data.questions[idx] = { ...data.questions[idx], answer, answeredAt: Date.now() };
270
+ this.writeQuestions(org, data);
271
+ const running = this.orgs.get(org);
272
+ if (running) {
273
+ // Org IS running — deliver or report a real error, but never fall through to the
274
+ // offline queue+autoWake path below: autoWake() no-ops when this.orgs already has
275
+ // the org (see its own guard), so a role-specific delivery failure here (mailbox
276
+ // closed, role unknown) would otherwise queue the answer forever with no real error
277
+ // and no delivery. Mirrors deliver()'s existing "shutting down" error for the same
278
+ // mid-shutdown-mailbox-closed race.
279
+ const agent = running.agents.get(role);
280
+ if (!agent)
281
+ return { ok: false, error: `role "${role}" not found in org "${org}"` };
282
+ if (agent.mailbox.isClosed)
283
+ return { ok: false, error: `role "${role}" in org "${org}" is shutting down — answer not delivered` };
284
+ running.bus.emit({ type: 'status', from: role, msg: 'question answered', data: { questionId } });
285
+ agent.mailbox.push(`[answer from human] question: ${data.questions[idx].question}\n\nanswer: ${answer}`);
286
+ return { ok: true };
287
+ }
288
+ // Org not running at all — queue for delivery on next start, matching deliver()'s
289
+ // existing offline fallback exactly (inbox.ts + autoWake).
290
+ if (!this.hasOrgDef(org))
291
+ return { ok: false, error: `org "${org}" not found (no saved definition)` };
292
+ queueMessage(this.root, org, {
293
+ fromQualified: 'human', toRole: role,
294
+ subject: `answer:${questionId}`,
295
+ body: `question: ${data.questions[idx].question}\n\nanswer: ${answer}`,
296
+ ts: Date.now(),
297
+ });
298
+ this.autoWake(org);
299
+ return { ok: true };
300
+ }
226
301
  /** Start an offline org in the background so queued messages get drained.
227
302
  * Fire-and-forget — errors are logged but don't propagate to the sender. */
228
303
  autoWake(name) {
@@ -3,7 +3,7 @@ import { readFileSync, existsSync } from 'node:fs';
3
3
  import { basename, extname, dirname, join } from 'node:path';
4
4
  /**
5
5
  * Forwards every bus event to the running mastermind control server
6
- * (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
6
+ * (src/ui/server.mjs, POST /api/mastermind/event) so the existing
7
7
  * dashboard shows org activity. Best-effort: failures are dropped.
8
8
  *
9
9
  * Events are translated into the dashboard's native vocabulary
@@ -16,7 +16,7 @@ import { basename, extname, dirname, join } from 'node:path';
16
16
  * companionEvents() additionally emits session:start/session:complete: the
17
17
  * dashboard's client-side Chat tab only lists a run in its session dropdown
18
18
  * once it has seen a session:start for that session id — org:start alone is
19
- * NOT enough (verified against dist/src/ui/orgs.html's handleMmEvent, which
19
+ * NOT enough (verified against src/ui/orgs.html's handleMmEvent, which
20
20
  * creates the chatSessions entry solely on session:start). Companions are
21
21
  * sent before the primary translate() payload for the same bus event so the
22
22
  * client-side session record exists before anything tries to append to it.
@@ -51,7 +51,7 @@ export function attachForwarder(bus, controlJsonPath = '.monomind/control.json')
51
51
  return null;
52
52
  }
53
53
  };
54
- // dist/src/ui/server.mjs's startServer() writes a per-process auth credential
54
+ // src/ui/server.mjs's startServer() writes a per-process auth credential
55
55
  // to 'dashboard-token' next to control.json (same .monomind dir) and requires
56
56
  // it via a header on every non-GET request. Read fresh each POST since the
57
57
  // credential rotates whenever the server restarts.
@@ -146,6 +146,10 @@ export function translate(e) {
146
146
  },
147
147
  };
148
148
  }
149
+ case 'question': {
150
+ const q = e.data ?? {};
151
+ return { ...base, type: 'org:question', from: e.from, questionId: q.questionId, question: q.question };
152
+ }
149
153
  case 'status': {
150
154
  const msg = e.msg ?? '';
151
155
  const kind = classifyStatus(msg);
@@ -27,6 +27,30 @@ export async function startOrgServer(daemon, port = 0) {
27
27
  }
28
28
  });
29
29
  }
30
+ else if (req.method === 'POST' && req.url === '/api/answer-question') {
31
+ let body = '';
32
+ req.on('data', c => { body += c; });
33
+ req.on('end', () => {
34
+ (async () => {
35
+ try {
36
+ const payload = JSON.parse(body || '{}');
37
+ if (!payload.org || !payload.role || !payload.questionId || payload.answer === undefined) {
38
+ res.writeHead(400, { 'Content-Type': 'application/json' });
39
+ res.end(JSON.stringify({ ok: false, error: 'org, role, questionId, answer are required' }));
40
+ return;
41
+ }
42
+ const result = await daemon.answerQuestion(payload.org, payload.role, payload.questionId, payload.answer);
43
+ res.writeHead(result.ok ? 200 : 404, { 'Content-Type': 'application/json' });
44
+ res.end(JSON.stringify(result));
45
+ }
46
+ catch (err) {
47
+ res.writeHead(400, { 'Content-Type': 'application/json' });
48
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : 'bad request' }));
49
+ }
50
+ })();
51
+ });
52
+ return;
53
+ }
30
54
  else {
31
55
  res.writeHead(404);
32
56
  res.end('not found');
@@ -12,6 +12,7 @@ export interface SessionOpts {
12
12
  mailbox: Mailbox;
13
13
  cwd: string;
14
14
  deliver: DeliverFn;
15
+ askHuman?: (role: string, question: string) => Promise<string>;
15
16
  def?: OrgDef;
16
17
  maxTurns?: number;
17
18
  queryFn?: typeof query;
@@ -12,6 +12,7 @@ export function buildRolePrompt(role, def, roster) {
12
12
  `## Communication protocol`,
13
13
  `The ONLY way to communicate with other agents is the org_send tool.`,
14
14
  `Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
15
+ `If you need a human decision, call ask_human with your question, then end your turn — you'll receive the human's answer as a new message when it arrives. Do not call ask_human for anything you can resolve yourself.`,
15
16
  `When you receive a message, act on it, then org_send your result to the requester.`,
16
17
  `When your current work is complete and no reply is needed, end your turn without further tool calls.`,
17
18
  ].filter(Boolean).join('\n\n');
@@ -51,6 +52,13 @@ async function runOneSession(opts) {
51
52
  const receipt = await deliver(role.id, args.to, args.subject, args.message);
52
53
  return { content: [{ type: 'text', text: receipt }] };
53
54
  }),
55
+ tool('ask_human', 'Ask a human a free-form question and pause for their answer. Use only when you genuinely need human judgment.', { question: z.string() }, async (args) => {
56
+ if (!opts.askHuman) {
57
+ return { content: [{ type: 'text', text: 'ask_human is not available in this session' }] };
58
+ }
59
+ const receipt = await opts.askHuman(role.id, args.question);
60
+ return { content: [{ type: 'text', text: receipt }] };
61
+ }),
54
62
  ],
55
63
  });
56
64
  bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
@@ -832,7 +832,7 @@ export interface BusEvent {
832
832
  ts: number;
833
833
  org: string;
834
834
  run: string;
835
- type: 'message' | 'xorg' | 'tool' | 'asset' | 'chat' | 'status' | 'audit' | 'usage';
835
+ type: 'message' | 'xorg' | 'tool' | 'asset' | 'chat' | 'status' | 'audit' | 'usage' | 'question';
836
836
  from?: string;
837
837
  to?: string;
838
838
  subject?: string;
@@ -7,7 +7,7 @@
7
7
  * cw — cache-write tokens
8
8
  * cr — cache-read tokens
9
9
  *
10
- * Consumers: dist/src/ui/collector.mjs and dist/src/ui/server.mjs both
10
+ * Consumers: src/ui/collector.mjs and src/ui/server.mjs both
11
11
  * derive their inline pricing tables from this canonical list.
12
12
  */
13
13
  export interface ModelPrice {
@@ -640,15 +640,30 @@ function slugToPath(slug) {
640
640
  }
641
641
 
642
642
  // Reconstruct the real filesystem path from a Claude project slug.
643
- // Slugs encode the path with every '/' replaced by '-', which is lossy when
644
- // directory names contain literal hyphens (e.g. /Desktop/agent-f/accounting).
645
- // Strategy: naive replace first; if not found, greedy DFS through the filesystem
646
- // trying longest-possible segment (with embedded hyphens) at each level.
643
+ // Slugs encode the path with every '/' AND '.' replaced by '-' (so a hidden
644
+ // directory like '.claude' shows up as a bare '--claude', i.e. an empty split
645
+ // token immediately before 'claude' e.g. the real path
646
+ // '/monomind/.claude/worktrees/foo' slugs to '-monomind--claude-worktrees-foo').
647
+ // Also lossy when directory names contain literal hyphens (e.g.
648
+ // /Desktop/agent-f/accounting). Strategy: naive replace first; if not found,
649
+ // greedy DFS through the filesystem trying longest-possible segment (with
650
+ // embedded hyphens) at each level.
647
651
  function resolveSlugPath(slug) {
648
652
  const naive = '/' + slug.replace(/^-/, '').replace(/-/g, '/');
649
653
  try { if (fs.existsSync(naive)) return naive; } catch {}
650
654
 
651
- const tokens = slug.replace(/^-/, '').split('-').filter(Boolean);
655
+ // An empty token between two hyphens marks a collapsed '.' — reattach it to
656
+ // the following token instead of dropping it, so hidden directories survive
657
+ // reconstruction (dropping it silently turns '.claude' into 'claude', which
658
+ // never exists on disk, sending the walk down the wrong path entirely).
659
+ const rawTokens = slug.replace(/^-/, '').split('-');
660
+ const tokens = [];
661
+ let pendingDot = false;
662
+ for (const part of rawTokens) {
663
+ if (part === '') { pendingDot = true; continue; }
664
+ tokens.push(pendingDot ? '.' + part : part);
665
+ pendingDot = false;
666
+ }
652
667
 
653
668
  function walk(idx, dir) {
654
669
  if (idx === tokens.length) return dir;