moshcode 0.33.0 → 0.34.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.
package/README.md CHANGED
@@ -125,10 +125,68 @@ moshcode attach api # step in; Ctrl-b d steps back out
125
125
  moshcode kill api # end it
126
126
  ```
127
127
 
128
+ ### A workspace: a few shells and an agent
129
+
130
+ This is what most people actually want — a couple of shells to work in and an
131
+ agent or two running beside them, none of which die when the terminal does:
132
+
133
+ ```sh
134
+ cd ~/src/coinpay
135
+ moshcode herd shell --name work # a plain $SHELL
136
+ moshcode herd shell --name logs # another
137
+ moshcode agents claude -d --name api # and an agent
138
+ ```
139
+
140
+ ```sh
141
+ $ moshcode ps
142
+ api claude blocked ~/src/coinpay 3m
143
+ logs shell idle ~/src/coinpay 3m
144
+ work shell idle ~/src/coinpay 3m
145
+
146
+ ⚠ 1 waiting on you — moshcode attach api
147
+ ```
148
+
149
+ `moshcode attach work` puts you in one; `Ctrl-b s` hops between all three;
150
+ `Ctrl-b d` leaves the lot running. Close the laptop and they are still there.
151
+
152
+ ### Agents moshcode does not ship
153
+
154
+ `start` only knows the engines moshcode installs. `run` takes anything —
155
+ an agent with no install spec here, a build, a script:
156
+
157
+ ```sh
158
+ moshcode herd run --name cur -- cursor-agent
159
+ moshcode herd run --name build -- npm run watch
160
+ ```
161
+
162
+ Everything after `--` is the command, flags and all. These get a roster entry
163
+ and the same state detection as a known engine: the shared rules match what a
164
+ terminal *draws* — a y/n prompt, a numbered menu, "esc to interrupt" — not
165
+ anything engine-specific, so an agent moshcode has never heard of still shows up
166
+ `blocked` when it stops to ask you something.
167
+
128
168
  Close the terminal, drop the SSH link, come back tomorrow — `moshcode ps` still
129
169
  answers, and `moshcode attach` puts you back inside. In the pit the same verbs
130
170
  are `/ps`, `/attach`, `/kill`, and the roster prints on the way in.
131
171
 
172
+ ### Switching between them without leaving
173
+
174
+ Every session lives in one tmux server that moshcode owns, so once you are
175
+ attached to any of them you can move around the whole herd without going back to
176
+ the pit:
177
+
178
+ | key | |
179
+ |---|---|
180
+ | `Ctrl-b s` | pick from a list of every session |
181
+ | `Ctrl-b )` / `Ctrl-b (` | next / previous session |
182
+ | `Ctrl-b L` | back to the one you were just in |
183
+ | `Ctrl-b d` | detach — the session keeps running |
184
+
185
+ That server is started without your `~/.tmux.conf`, so these are the stock
186
+ bindings whatever your own tmux does with the prefix. Under the no-tmux fallback
187
+ there is no switcher: detach with `Ctrl-]` and `moshcode attach <name>` the next
188
+ one.
189
+
132
190
  ### Which one needs you
133
191
 
134
192
  Every session carries a state: `working`, `blocked`, `done`, `idle`, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
@@ -79,12 +79,25 @@ export const CORE_CLI_COMMANDS = [
79
79
  verbs: "HERD_VERBS",
80
80
  flags: [["--json", "machine-readable, on every verb", ""]],
81
81
  examples: [
82
- ["moshcode herd start claude --name api", "start one and get your prompt back"],
83
- ["moshcode herd prompt api \"run the tests\" --wait", "hand it work and block until it lands"],
84
- ["moshcode herd read api --lines 40", "read its screen without attaching"],
82
+ ["# a workspace: two shells and an agent, none of which die with this terminal", ""],
83
+ ["moshcode herd shell --name work", "a plain $SHELL you can come back to"],
84
+ ["moshcode herd shell --name logs", "another one"],
85
+ ["moshcode agents claude -d --name api", "an agent, detached"],
86
+ ["moshcode ps", "all three, and which one is blocked"],
87
+ ["moshcode attach api", "step in · Ctrl-b s switches · Ctrl-b d leaves it running"],
88
+ ["", ""],
89
+ ["# an agent moshcode has no install spec for", ""],
90
+ ["moshcode herd run --name cur -- cursor-agent", "anything at all runs in the herd"],
91
+ ["", ""],
92
+ ["# driving one without attaching", ""],
93
+ ["moshcode herd prompt api \"run the tests\" --wait", "hand it work, block until it lands"],
94
+ ["moshcode herd read api --lines 40", "read its screen"],
85
95
  ],
86
96
  seeAlso: ["ps", "attach", "wait", "restore", "start"],
87
- note: "sessions live in a tmux server moshcode owns, or under script(1) when there is no tmux. with neither, launches stay in the foreground and say so.",
97
+ note: "`start` is for the engines moshcode installs; `run` and `shell` take anything else, "
98
+ + "so an agent it has never heard of still gets a roster entry and blocked/idle detection. "
99
+ + "sessions live in a tmux server moshcode owns, or under script(1) when there is no tmux. "
100
+ + "with neither, launches stay in the foreground and say so.",
88
101
  },
89
102
  {
90
103
  name: "ps",
@@ -103,7 +116,10 @@ export const CORE_CLI_COMMANDS = [
103
116
  synopsis: [["moshcode attach <name>", "detach again with Ctrl-b d (or Ctrl-] without tmux)"]],
104
117
  examples: [["moshcode attach api", ""]],
105
118
  seeAlso: ["ps", "herd", "kill"],
106
- note: "detaching leaves the session running. ending it is `moshcode kill`.",
119
+ note: "detaching leaves the session running; ending it is `moshcode kill`. "
120
+ + "the whole herd shares one tmux server, so from inside any session Ctrl-b s picks another, "
121
+ + "Ctrl-b ) and Ctrl-b ( step through them, and Ctrl-b L goes back to the last one — "
122
+ + "no switcher under the no-tmux fallback, where Ctrl-] detaches instead.",
107
123
  },
108
124
  {
109
125
  name: "kill",
@@ -720,6 +736,16 @@ export const HERD_VERBS = [
720
736
  ["--agent", "autonomous mode — bypasses the engine's approvals", ""],
721
737
  ["--json", "machine-readable", ""],
722
738
  ] },
739
+ { name: "run", description: "run ANY command in the herd — an agent moshcode does not ship, a build, a script",
740
+ synopsis: [["moshcode herd run [--name <slug>] -- <command…>", "everything after -- is the command"]],
741
+ flags: [
742
+ ["--name <slug>", "session name", "<command>-<dir>"],
743
+ ["--cwd <dir>", "where to run it", "this directory"],
744
+ ["--json", "machine-readable", ""],
745
+ ] },
746
+ { name: "shell", description: "a plain $SHELL in the herd",
747
+ synopsis: [["moshcode herd shell [--name <slug>]", ""]],
748
+ flags: [["--name <slug>", "session name", "shell-<dir>"], ["--cwd <dir>", "where to run it", "this directory"]] },
723
749
  { name: "attach", description: "put this terminal inside a session",
724
750
  synopsis: [["moshcode herd attach <name>", ""]] },
725
751
  { name: "kill", description: "end a session",
package/src/help.mjs CHANGED
@@ -167,7 +167,11 @@ export function wrap(text, indent = 0, width = WIDTH) {
167
167
 
168
168
  /** ` name description`, wrapped, for a two-column list. */
169
169
  function row(left, right, pad = 22) {
170
- const head = ` ${left.padEnd(pad)}`;
170
+ // padEnd does nothing when the left column is already wider than the pad, so
171
+ // a long example used to butt straight against its comment:
172
+ // `… "run the tests" --wait# hand it work`. One guaranteed space keeps the
173
+ // two readable however long the command gets.
174
+ const head = left.length >= pad ? ` ${left} ` : ` ${left.padEnd(pad)}`;
171
175
  if (!right) return head.trimEnd();
172
176
  const wrapped = wrap(right, head.length, WIDTH);
173
177
  return `${head}${wrapped}`;
package/src/herd-cli.mjs CHANGED
@@ -11,7 +11,7 @@ import path from "node:path";
11
11
  import {
12
12
  attachSession, capture, defaultName, detectSubstrate, forgetSession, HERD_SOCKET,
13
13
  herdDir, killSession, listSessions, readManifest, rememberSession, sendKeys, sendPrompt,
14
- startSession, stopRuntime, substrateNote, validName, NAME_RE,
14
+ slugifyName, startSession, stopRuntime, substrateNote, validName, NAME_RE,
15
15
  } from "./herd.mjs";
16
16
  import { clearReport, reportState, STATES, withState } from "./herd-state.mjs";
17
17
  import { ENGINES, resolveEngine, resolveExecutable, agentLaunchArgs } from "./engines.mjs";
@@ -182,6 +182,79 @@ export function herdStart(argv, { write = console.log } = {}) {
182
182
  return EXIT.matched;
183
183
  }
184
184
 
185
+ /**
186
+ * Run anything at all in the herd — a shell, or an agent moshcode does not ship
187
+ * an install spec for.
188
+ *
189
+ * `herd start` is deliberately limited to the engines in ENGINES, because it
190
+ * does engine-specific things: agent-mode flags, env stripping, resume args.
191
+ * That made the herd useless for the two most common things people actually
192
+ * want in it — a couple of shells, and whichever agent they use that moshcode
193
+ * has never heard of (cursor-agent, copilot, amp, a local script).
194
+ *
195
+ * So this is the escape hatch, and it is the same model herdr uses: a session
196
+ * holds a process, and an agent is just a process we happen to recognise.
197
+ * Detection still works, because the shared rules in herd-state.mjs match what
198
+ * a terminal draws — a y/n prompt, a numbered menu, "esc to interrupt" — rather
199
+ * than anything engine-specific. An unknown agent that stops to ask a question
200
+ * shows up `blocked` without moshcode knowing what it is.
201
+ */
202
+ export function herdRun(argv, { write = console.log, shell = false } = {}) {
203
+ const substrate = requireSubstrate(write);
204
+ if (!substrate) return EXIT.usage;
205
+
206
+ const flags = { name: null, cwd: process.cwd(), json: false };
207
+ const command = [];
208
+ let afterSeparator = false;
209
+ for (let i = 0; i < argv.length; i++) {
210
+ const a = argv[i];
211
+ if (afterSeparator) { command.push(a); continue; }
212
+ // Everything after `--` belongs to the command, flags included — otherwise
213
+ // `herd run -- claude --json` would have its --json eaten by us.
214
+ if (a === "--") { afterSeparator = true; }
215
+ else if (a === "--name") flags.name = argv[++i];
216
+ else if (a.startsWith("--name=")) flags.name = a.slice(7);
217
+ else if (a === "--cwd") flags.cwd = path.resolve(argv[++i] || ".");
218
+ else if (a === "--json") flags.json = true;
219
+ else command.push(a);
220
+ }
221
+
222
+ if (shell && !command.length) {
223
+ command.push(process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/sh"));
224
+ }
225
+ if (!command.length) {
226
+ write(err('usage: moshcode herd run [--name <slug>] -- <command…>'));
227
+ write(info(`e.g. ${acid('moshcode herd run --name build -- npm run watch')}`));
228
+ return EXIT.usage;
229
+ }
230
+
231
+ const [bin, ...args] = command;
232
+ const label = shell ? "shell" : path.basename(bin);
233
+ const taken = listSessions().map((s) => s.name);
234
+ const name = flags.name || defaultName(slugifyName(label), flags.cwd, taken);
235
+ if (!validName(name)) {
236
+ write(err(`invalid name ${JSON.stringify(name)} — must match ${NAME_RE}`));
237
+ return EXIT.usage;
238
+ }
239
+
240
+ const started = startSession({ name, engine: label, bin, args, cwd: flags.cwd, substrate });
241
+ if (!started.ok) {
242
+ write(err(String(started.error?.message || started.error)));
243
+ return EXIT.usage;
244
+ }
245
+
246
+ if (flags.json) {
247
+ write(JSON.stringify({ name, engine: label, cwd: flags.cwd, substrate }, null, 2));
248
+ return EXIT.matched;
249
+ }
250
+ write(ok(`${bone(name)} — ${label} running in the herd. the prompt is yours.`));
251
+ write(info(`attach: ${acid(`moshcode attach ${name}`)} · roster: ${acid("moshcode ps")}`));
252
+ return EXIT.matched;
253
+ }
254
+
255
+ /** A plain $SHELL in the herd — the common case of herdRun. */
256
+ export const herdShell = (argv, options = {}) => herdRun(argv, { ...options, shell: true });
257
+
185
258
  /**
186
259
  * Pull the herd flags out of an engine launch (PRD 0009 R3).
187
260
  *
@@ -646,7 +719,8 @@ export function herdStop(argv, { write = console.log } = {}) {
646
719
 
647
720
  const VERBS = {
648
721
  ps: herdPs, list: herdPs, status: herdStatus,
649
- start: herdStart, attach: herdAttach, kill: herdKill, prune: herdPrune,
722
+ start: herdStart, run: herdRun, shell: herdShell,
723
+ attach: herdAttach, kill: herdKill, prune: herdPrune,
650
724
  read: herdRead, prompt: herdPrompt, "send-keys": herdSendKeys,
651
725
  wait: herdWait, restore: herdRestore, report: herdReport,
652
726
  notify: herdNotify, watch: herdWatch, stop: herdStop,
@@ -80,7 +80,25 @@ export const COMMON_RULES = {
80
80
  /\bctrl\+c to (?:stop|cancel|interrupt)\b/i,
81
81
  /\bpress esc to cancel\b/i,
82
82
  ],
83
- idle: [],
83
+ // A prompt sitting at the very end of the screen, waiting for a keystroke —
84
+ // a shell's `$`, a root `#`, zsh/starship's `❯`/`➜`, an agent's `>` composer.
85
+ // Anchored to the end of the *capture* rather than to any line, because a `$`
86
+ // in the middle of output is a dollar sign and not an invitation.
87
+ //
88
+ // Checked after blocked and working, so the cost of a false positive is only
89
+ // `idle` where `unknown` was already the honest answer. This is what stops a
90
+ // couple of shells in the herd reading `unknown` forever.
91
+ idle: [
92
+ // The glyph last: `… $`, `… #`, `… ❯`.
93
+ /[$%#>❯➜»]\s*$/,
94
+ // The glyph first, with the path after it: `➜ ~/src/api `, which is what
95
+ // zsh and starship actually draw. Restricted to ❯ and ➜ because those are
96
+ // prompt characters and almost nothing else; `>` and `#` in that position
97
+ // are markdown quotes and headings, which agents print all the time.
98
+ // Claude Code's `❯ 2. Dark mode` selector reaches the blocked rule first,
99
+ // so a menu still reads blocked rather than idle.
100
+ /(?:^|\n)[^\n]*[❯➜][^\n]*$/,
101
+ ],
84
102
  };
85
103
 
86
104
  /**