monomind 2.1.3 → 2.1.4
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/README.md +18 -17
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +91 -701
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
- package/packages/@monomind/cli/README.md +18 -17
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +18 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +76 -1
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +7 -3
- package/packages/@monomind/cli/dist/src/orgrt/server.js +24 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -0
- package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +270 -499
- package/packages/@monomind/cli/dist/src/ui/orgs-files.js +1 -1
- package/packages/@monomind/cli/dist/src/ui/server.mjs +132 -158
- package/packages/@monomind/cli/package.json +3 -3
- package/packages/@monomind/cli/scripts/publish.sh +6 -0
- 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
|
|
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,
|
|
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:
|
|
53
|
-
" Goal:
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
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 |
|
|
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
|
|
87
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
[ -
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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.
|
|
52
|
-
| **Dependencies** | Standard npm packages (tree-sitter, sql.js, zod).
|
|
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,
|
|
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[("
|
|
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
|
|
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
|
-
| **
|
|
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 "..."] [--
|
|
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 [--
|
|
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
|
|
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
|
-
## 📋
|
|
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 (
|
|
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
|
-
* (
|
|
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
|
|
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
|
-
//
|
|
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 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:
|
|
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 '-'
|
|
644
|
-
// directory
|
|
645
|
-
//
|
|
646
|
-
//
|
|
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
|
-
|
|
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;
|