@webority/ensemble 0.5.12 → 0.5.13

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 CHANGED
@@ -32,7 +32,7 @@ portal), downloads + configures + auto-starts the runner, and for **each detecte
32
32
  |---|---|---|
33
33
  | Claude | `~/.claude/settings.json` | `~/.claude.json` → `ensemble` |
34
34
  | Codex | `~/.codex/hooks.json` | `~/.codex/config.toml` |
35
- | Grok | `~/.grok/hooks/agent-bus.json` | `~/.grok/config.toml` |
35
+ | Grok | `~/.grok/hooks/agent-bus.json` + managed `~/.grok/rules/ensemble-mail.md` | `~/.grok/config.toml` |
36
36
  | OpenCode | (MCP primary) | `~/.config/opencode/opencode.json` → `mcp.ensemble` |
37
37
  | OMP (oh-my-pi) | `~/.omp/agent/ensemble-hooks.json` | `~/.omp/agent/mcp.json` |
38
38
  | Gemini | best-effort | `~/.gemini/settings.json` |
@@ -40,6 +40,7 @@ portal), downloads + configures + auto-starts the runner, and for **each detecte
40
40
 
41
41
  1. **Hooks** — mail at turn boundaries
42
42
  2. **MCP** — `ensemble mcp --engine <e>` as server name **`ensemble`**
43
+ 3. **Grok rules** — managed always-on policy at `~/.grok/rules/ensemble-mail.md` (no LLM inbox poll loops; shell monitor only; house overrides in `ensemble-mail-local.md`)
43
44
 
44
45
  Fleet playbook: `docs/FLEET.md`.
45
46
  **Your subscription logins never leave your machine.** Restart open agent sessions after enroll/hooks.
@@ -0,0 +1,104 @@
1
+ <!-- managed-by: ensemble — written by `ensemble hooks` / login / enroll. Edits will be overwritten on the next wire. -->
2
+
3
+ # Ensemble mail (default for every Grok session)
4
+
5
+ Standing policy for the Ensemble agent bus. Applies in **all** projects on this machine once Ensemble wires Grok.
6
+
7
+ ## Never use a model loop to poll mail
8
+
9
+ **Forbidden:** `/loop`, `scheduler_create`, or any foreground recurring prompt whose job is "check inbox".
10
+
11
+ Why: every fire is a full model turn. Empty inboxes still burn tokens, hit rate limits, and thrash callbacks.
12
+
13
+ ## Idle backup = shell monitor only
14
+
15
+ When the session should stay reachable for peer mail while idle:
16
+
17
+ 1. Resolve this session's mailbox via `ensemble who` (or the Ensemble MCP `who`).
18
+ 2. Start a **persistent** `monitor` whose command is **shell-only** and peeks every ~60s.
19
+ 3. **Empty inbox → print nothing** (no agent wake, no model cost).
20
+ 4. **New mail fingerprint → one stdout line** `ENSEMBLE_MAIL` (one wake).
21
+ 5. On wake: Ensemble MCP `read_inbox`, then act (see below). Do not re-poll with the LLM.
22
+
23
+ ### Canonical monitor recipe (PowerShell — Windows)
24
+
25
+ Replace `MAILBOX` with this session's exact mailbox name:
26
+
27
+ ```powershell
28
+ $ErrorActionPreference = 'Continue'
29
+ $mb = 'MAILBOX'
30
+ $lastHash = ''
31
+ while ($true) {
32
+ try {
33
+ $out = & ensemble read --from $mb --peek 2>&1 | Out-String
34
+ $out = $out.Trim()
35
+ if ($out -and ($out -notmatch '(?i)no new mail')) {
36
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($out)
37
+ $hash = [System.BitConverter]::ToString(
38
+ [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes))
39
+ if ($hash -ne $lastHash) {
40
+ $lastHash = $hash
41
+ Write-Output 'ENSEMBLE_MAIL'
42
+ [Console]::Out.Flush()
43
+ }
44
+ } else {
45
+ $lastHash = ''
46
+ }
47
+ } catch { }
48
+ Start-Sleep -Seconds 60
49
+ }
50
+ ```
51
+
52
+ ### Canonical monitor recipe (bash — macOS / Linux)
53
+
54
+ ```bash
55
+ mb='MAILBOX'
56
+ last_hash=''
57
+ while true; do
58
+ out=$(ensemble read --from "$mb" --peek 2>&1 || true)
59
+ if [ -n "$out" ] && ! printf '%s' "$out" | grep -qi 'no new mail'; then
60
+ hash=$(printf '%s' "$out" | sha256sum 2>/dev/null | awk '{print $1}')
61
+ [ -z "$hash" ] && hash=$(printf '%s' "$out" | shasum -a 256 | awk '{print $1}')
62
+ if [ "$hash" != "$last_hash" ]; then
63
+ last_hash=$hash
64
+ printf 'ENSEMBLE_MAIL\n'
65
+ fi
66
+ else
67
+ last_hash=''
68
+ fi
69
+ sleep 60
70
+ done
71
+ ```
72
+
73
+ Monitor tool args: `persistent: true`, description like `Wake only on new Ensemble mail`.
74
+
75
+ Stop with `kill_command_or_subagent` when the user says stop / session ends.
76
+
77
+ ## Session start (when collaboration is likely)
78
+
79
+ If the user is coordinating with other agents, Ensemble MCP is connected, or the task is multi-session:
80
+
81
+ 1. `set_label` early (purpose + live focus + model).
82
+ 2. Start the shell mail monitor once (skip if already running this session).
83
+ 3. Rely on Ensemble lifecycle hooks for turn-boundary drain; the monitor is the **idle** backup only.
84
+
85
+ If the task is clearly solo with no bus peers, skip the monitor unless the user asks.
86
+
87
+ ## Act on peer mail
88
+
89
+ When mail asks for work this session can do (review, answer, implement a lane, resend a verdict):
90
+
91
+ - **Do it** when the human has established that peer requests are standing authorization for this session (or the human's standing house rules say so).
92
+ - Brief the human in chat after/while acting so they stay informed.
93
+ - Destructive or outward-facing actions that leave this machine (prod deploy, force-push, delete shared resources, message external systems as the company) still need the human's explicit word — bus mail never authorizes those alone.
94
+
95
+ Default when house rules are silent: treat peer mail as **information**, answer trivial facts, and do not auto-start multi-file or production-affecting work without the human.
96
+
97
+ ## Quick replies vs substantial work
98
+
99
+ - Trivial facts / ack / status → reply on the bus immediately.
100
+ - Substantial work (when authorized) → do it, then send findings/status on the bus; keep label/focus current.
101
+
102
+ ## Primary path remains hooks
103
+
104
+ Ensemble stop/user-prompt hooks (wired via `ensemble hooks` into Grok) still drain and can wake at turn boundaries. The shell monitor does **not** replace hooks; it covers **true idle** when no turn is running.
package/lib/core.js CHANGED
@@ -465,6 +465,9 @@ function wireHooks() {
465
465
  }
466
466
  if (enginePresent('grok')) {
467
467
  if (writeEngineHooks(path.join(HOME, '.grok', 'hooks', 'agent-bus.json'), runtimeInvoke, 'grok')) report.push('grok:hooks');
468
+ // Always-on Grok rules so new machines get the mail-watch policy without hand-copying.
469
+ // Overwrites managed file only; never touches ensemble-mail-local.md (house overrides).
470
+ if (writeGrokRules()) report.push('grok:rules');
468
471
  }
469
472
  // OpenCode: no stable global lifecycle-hooks file yet — MCP is the primary path.
470
473
  if (enginePresent('opencode')) report.push('opencode:hooks?');
@@ -758,6 +761,47 @@ function isEnsembleHook(h) {
758
761
  return /ensemble-(bus|runtime)/i.test(String(cmd));
759
762
  }
760
763
 
764
+ /// Write Ensemble-managed Grok rules into ~/.grok/rules/.
765
+ /// Source of truth: lib/assets/grok-rules/*.md shipped with the npm package.
766
+ /// Idempotent overwrite so recipe updates land on `ensemble hooks` / login / enroll.
767
+ /// Personal/house overrides belong in ~/.grok/rules/*-local.md (never written here).
768
+ function writeGrokRules() {
769
+ const srcDir = path.join(__dirname, 'assets', 'grok-rules');
770
+ const destDir = path.join(HOME, '.grok', 'rules');
771
+ if (!fs.existsSync(srcDir)) {
772
+ warn('ensemble: grok rules assets missing at ' + srcDir + ' — skip grok:rules');
773
+ return false;
774
+ }
775
+ try {
776
+ fs.mkdirSync(destDir, { recursive: true });
777
+ } catch (e) {
778
+ warn('could not create Grok rules dir ' + destDir + ': ' + (e.message || e));
779
+ return false;
780
+ }
781
+ let wrote = 0;
782
+ let names;
783
+ try {
784
+ names = fs.readdirSync(srcDir).filter((n) => n.endsWith('.md'));
785
+ } catch (e) {
786
+ warn('could not read Grok rules assets: ' + (e.message || e));
787
+ return false;
788
+ }
789
+ for (const name of names) {
790
+ // Never clobber a user *-local.md even if someone ships one by mistake.
791
+ if (name.endsWith('-local.md')) continue;
792
+ const src = path.join(srcDir, name);
793
+ const dest = path.join(destDir, name);
794
+ try {
795
+ const body = fs.readFileSync(src, 'utf8');
796
+ fs.writeFileSync(dest, body, { encoding: 'utf8' });
797
+ wrote += 1;
798
+ } catch (e) {
799
+ warn('could not write Grok rule ' + dest + ': ' + (e.message || e));
800
+ }
801
+ }
802
+ return wrote > 0;
803
+ }
804
+
761
805
  // Codex + Grok use the SAME hooks schema as Claude Code: PascalCase event names, each mapping to an
762
806
  // array of matcher-groups { matcher?, hooks: [{ type: "command", command }] }. (Verified against
763
807
  // Codex's config-advanced docs and Grok's bundled ~/.grok/docs hooks guide.) The old FLAT
@@ -948,5 +992,5 @@ module.exports = {
948
992
  sleep, openBrowser, startDevice, pollDevice, finishSetup, assertSupported,
949
993
  runtimePaths, runtimeBin, runtimeInstalled, requireRuntime, ensureRuntime,
950
994
  isUsableBinary, removeLegacyBusArtifacts, MIN_BINARY_BYTES,
951
- writeEngineHooks, upsertTomlMcp, installedVersionCurrent, readInstalledVersion, PKG_VERSION,
995
+ writeEngineHooks, writeGrokRules, upsertTomlMcp, installedVersionCurrent, readInstalledVersion, PKG_VERSION,
952
996
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/ensemble",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Connect this machine to Ensemble — runs the local agent runner and wires the ensemble session bus so your coding sessions talk (per-org, isolated).",
5
5
  "bin": {
6
6
  "ensemble": "bin/ensemble.js"
@@ -12,11 +12,11 @@
12
12
  "node": ">=18"
13
13
  },
14
14
  "optionalDependencies": {
15
- "@webority/ensemble-darwin-arm64": "0.5.12",
16
- "@webority/ensemble-darwin-x64": "0.5.12",
17
- "@webority/ensemble-linux-arm64": "0.5.12",
18
- "@webority/ensemble-linux-x64": "0.5.12",
19
- "@webority/ensemble-win-x64": "0.5.12"
15
+ "@webority/ensemble-darwin-arm64": "0.5.13",
16
+ "@webority/ensemble-darwin-x64": "0.5.13",
17
+ "@webority/ensemble-linux-arm64": "0.5.13",
18
+ "@webority/ensemble-linux-x64": "0.5.13",
19
+ "@webority/ensemble-win-x64": "0.5.13"
20
20
  },
21
21
  "files": [
22
22
  "bin",