@moxxy/cli 0.8.0 → 0.8.2

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/dist/bin.js CHANGED
@@ -194,6 +194,14 @@ var init_log = __esm({
194
194
  listeners = /* @__PURE__ */ new Set();
195
195
  clearListeners = /* @__PURE__ */ new Set();
196
196
  now;
197
+ /**
198
+ * Seq of the FIRST event this log holds. 0 for an authoring log; a mirror
199
+ * primed by a partial attach replay (runner protocol v6 `replay.start`)
200
+ * rebases to the first replayed seq so `ingest`'s contiguity gate lines up
201
+ * with the runner's stream instead of expecting history we never received.
202
+ * `seq === base + index` for every held event.
203
+ */
204
+ base = 0;
197
205
  constructor(seed = [], opts = {}) {
198
206
  this.now = opts.now ?? Date.now;
199
207
  for (const e3 of seed)
@@ -202,13 +210,18 @@ var init_log = __esm({
202
210
  get length() {
203
211
  return this.events.length;
204
212
  }
213
+ /** Seq of the first held event (see {@link rebase}). */
214
+ get baseSeq() {
215
+ return this.base;
216
+ }
205
217
  at(seq) {
206
- if (seq < 0 || seq >= this.events.length)
218
+ const index = seq - this.base;
219
+ if (index < 0 || index >= this.events.length)
207
220
  return void 0;
208
- return this.events[seq];
221
+ return this.events[index];
209
222
  }
210
- slice(from = 0, to = this.events.length) {
211
- return this.events.slice(from, to);
223
+ slice(from = 0, to = this.base + this.events.length) {
224
+ return this.events.slice(Math.max(0, from - this.base), Math.max(0, to - this.base));
212
225
  }
213
226
  ofType(type) {
214
227
  return this.events.filter((e3) => e3.type === type);
@@ -220,7 +233,7 @@ var init_log = __esm({
220
233
  return [...this.events];
221
234
  }
222
235
  async append(partial) {
223
- const event = materializeEvent(partial, this.events.length, this.now);
236
+ const event = materializeEvent(partial, this.base + this.events.length, this.now);
224
237
  this.events.push(event);
225
238
  const snapshot = [...this.listeners];
226
239
  for (const fn of snapshot) {
@@ -245,7 +258,7 @@ var init_log = __esm({
245
258
  * Not for normal authoring; use {@link append} for that.
246
259
  */
247
260
  ingest(event) {
248
- if (event.seq !== this.events.length)
261
+ if (event.seq !== this.base + this.events.length)
249
262
  return;
250
263
  this.events.push(event);
251
264
  const snapshot = [...this.listeners];
@@ -271,8 +284,25 @@ var init_log = __esm({
271
284
  * Safe to call only when no turn is in flight — callers should abort
272
285
  * their AbortController and await any pending runTurn() first.
273
286
  */
287
+ /**
288
+ * Start this (empty) log at `seq` instead of 0. A mirror primed by a
289
+ * PARTIAL attach replay (`replay: 'none'` / `{ tail }`) calls this with the
290
+ * runner's announced first seq so {@link ingest}'s contiguity gate accepts
291
+ * the stream. Only valid while empty — rebasing held events would detach
292
+ * their seqs from their indices.
293
+ */
294
+ rebase(seq) {
295
+ if (this.events.length > 0) {
296
+ throw new Error(`EventLog.rebase(${seq}): log already holds ${this.events.length} events`);
297
+ }
298
+ if (!Number.isInteger(seq) || seq < 0) {
299
+ throw new Error(`EventLog.rebase(${seq}): seq must be a non-negative integer`);
300
+ }
301
+ this.base = seq;
302
+ }
274
303
  clear() {
275
304
  this.events.length = 0;
305
+ this.base = 0;
276
306
  const snapshot = [...this.clearListeners];
277
307
  for (const fn of snapshot) {
278
308
  try {
@@ -466,9 +496,10 @@ async function runChildTurn(args) {
466
496
  return resolved.failure;
467
497
  const { strategy, strategyName } = resolved;
468
498
  const toolRegistry = spec.allowedTools && spec.allowedTools.length > 0 ? buildFilteredToolRegistry(parentSession.tools, new Set(spec.allowedTools)) : parentSession.tools;
499
+ const childModel = await resolveChildModel(rt3, spec, label3, childSessionId);
469
500
  const childLog = new EventLog();
470
- const spawner = createSubagentSpawner(rt3);
471
- const childCtx = buildChildContext(rt3, spec, childSessionId, childTurnId, toolRegistry, childLog, spawner);
501
+ const spawner = createSubagentSpawner({ ...rt3, parentModel: childModel });
502
+ const childCtx = buildChildContext(rt3, spec, childModel, childSessionId, childTurnId, toolRegistry, childLog, spawner);
472
503
  const capture = await executeChildLoop({
473
504
  rt: rt3,
474
505
  spec,
@@ -600,12 +631,24 @@ async function resolveStrategy(parentSession, parentTurnId, label3, childSession
600
631
  }
601
632
  };
602
633
  }
603
- function buildChildContext(rt3, spec, childSessionId, childTurnId, toolRegistry, childLog, spawner) {
604
- const { parentSession, parentSignal, parentModel } = rt3;
634
+ async function resolveChildModel(rt3, spec, label3, childSessionId) {
635
+ const { parentSession, parentTurnId, parentModel } = rt3;
636
+ const requested = spec.model;
637
+ if (requested === void 0 || requested === parentModel)
638
+ return parentModel;
639
+ const models = parentSession.providers.getActive().models;
640
+ if (models.length > 0 && !models.some((m3) => m3.id === requested)) {
641
+ await emitSubagentWarning(parentSession, parentTurnId, label3, childSessionId, `unknown model "${requested}" \u2014 falling back to parent model "${parentModel}"`);
642
+ return parentModel;
643
+ }
644
+ return requested;
645
+ }
646
+ function buildChildContext(rt3, spec, model, childSessionId, childTurnId, toolRegistry, childLog, spawner) {
647
+ const { parentSession, parentSignal } = rt3;
605
648
  return {
606
649
  sessionId: childSessionId,
607
650
  turnId: childTurnId,
608
- model: spec.model ?? parentModel,
651
+ model,
609
652
  ...spec.systemPrompt !== void 0 ? { systemPrompt: spec.systemPrompt } : {},
610
653
  provider: parentSession.providers.getActive(),
611
654
  tools: toolRegistry,
@@ -674,6 +717,7 @@ async function* runTurn(session, prompt, opts = {}) {
674
717
  const turnId = opts.turnId ?? session.startTurn().turnId;
675
718
  const provider = session.providers.getActive();
676
719
  const model = opts.model ?? provider.models[0]?.id ?? "default";
720
+ session.lastResolvedModel = model;
677
721
  const queue = [];
678
722
  const waiters = [];
679
723
  let done = false;
@@ -3023,6 +3067,11 @@ var init_session = __esm({
3023
3067
  elisionSettings = null;
3024
3068
  /** Lazy tool loading toggle, from `config.context.lazyTools`. Default off. */
3025
3069
  lazyTools = false;
3070
+ /**
3071
+ * Model id resolved by the most recent `runTurn()` (see SessionRuntime).
3072
+ * Last-writer-wins for concurrent turns; null until the first turn runs.
3073
+ */
3074
+ lastResolvedModel = null;
3026
3075
  /**
3027
3076
  * Live runtime capabilities the host installs on a local Session (see
3028
3077
  * SessionLike). A RemoteSession leaves them undefined. Declared here — rather
@@ -3638,7 +3687,7 @@ The body is the instructions for future invocations. Keep it under 30 lines. Num
3638
3687
  });
3639
3688
  async function synthesizeSkill(session, intent, scope, opts = {}) {
3640
3689
  const provider = session.providers.getActive();
3641
- const model = opts.model ?? provider.models[0]?.id ?? "claude-sonnet-4-6";
3690
+ const model = opts.model ?? session.lastResolvedModel ?? provider.models[0]?.id ?? "default";
3642
3691
  const draft = await draftSkill(provider, model, intent, session.signal);
3643
3692
  const baseDir = scope === "project" ? opts.projectDir ?? defaultProjectSkillsDir(session.cwd) : opts.userDir ?? defaultUserSkillsDir();
3644
3693
  await promises.mkdir(baseDir, { recursive: true });
@@ -134704,7 +134753,7 @@ function isRunnerUp(socketPath = runnerSocketPath()) {
134704
134753
 
134705
134754
  // ../runner/dist/server.js
134706
134755
  init_dist();
134707
- var RUNNER_PROTOCOL_VERSION = 5;
134756
+ var RUNNER_PROTOCOL_VERSION = 6;
134708
134757
  var MIN_COMPATIBLE_PROTOCOL_VERSION = 1;
134709
134758
  var RunnerMethod = {
134710
134759
  /** client->server: handshake; returns the initial info snapshot. */
@@ -134779,7 +134828,14 @@ var RunnerNotification = {
134779
134828
  * clear: post-reset events restart at seq 0, which a seq-contiguous
134780
134829
  * mirror only accepts from an empty log.
134781
134830
  */
134782
- SessionReset: "session.reset"
134831
+ SessionReset: "session.reset",
134832
+ /**
134833
+ * Sent once, immediately before the attach-time replay loop (v6): the
134834
+ * first seq this connection will replay/stream. The client rebases its
134835
+ * (empty) mirror to `fromSeq` so a partial replay (`replay: 'none'` /
134836
+ * `{ tail }`) ingests contiguously instead of dropping every event.
134837
+ */
134838
+ ReplayStart: "replay.start"
134783
134839
  };
134784
134840
  var attachmentSchema = z.object({
134785
134841
  kind: z.string(),
@@ -134790,14 +134846,22 @@ var attachmentSchema = z.object({
134790
134846
  var attachParamsSchema = z.object({
134791
134847
  protocolVersion: z.number(),
134792
134848
  role: z.string(),
134793
- sinceSeq: z.number().int().nonnegative().optional()
134849
+ sinceSeq: z.number().int().nonnegative().optional(),
134850
+ replay: z.union([
134851
+ z.literal("full"),
134852
+ z.literal("none"),
134853
+ z.object({ tail: z.number().int().positive() })
134854
+ ]).optional()
134794
134855
  });
134795
134856
  var runTurnParamsSchema = z.object({
134796
134857
  prompt: z.string(),
134797
134858
  model: z.string().optional(),
134798
134859
  systemPrompt: z.string().optional(),
134799
134860
  maxIterations: z.number().int().positive().optional(),
134800
- attachments: z.array(attachmentSchema).optional()
134861
+ attachments: z.array(attachmentSchema).optional(),
134862
+ // Bounded like the other id-bearing params so a hostile client can't stuff
134863
+ // an arbitrary blob into every event of the turn.
134864
+ turnId: z.string().min(1).max(120).optional()
134801
134865
  });
134802
134866
  var abortParamsSchema = z.object({ turnId: z.string() });
134803
134867
  var setResolverParamsSchema = z.object({
@@ -134945,7 +135009,11 @@ var RunnerServer = class {
134945
135009
  }
134946
135010
  client.role = params.role;
134947
135011
  client.attached = true;
134948
- for (const event of this.session.log.slice(0)) {
135012
+ const replay = params.replay ?? "full";
135013
+ const total = this.session.log.length;
135014
+ const start = replay === "full" ? 0 : replay === "none" ? total : Math.max(0, total - replay.tail);
135015
+ client.peer.notify(RunnerNotification.ReplayStart, { fromSeq: start });
135016
+ for (const event of this.session.log.slice(start)) {
134949
135017
  client.peer.notify(RunnerNotification.Event, { event });
134950
135018
  }
134951
135019
  return {
@@ -134956,7 +135024,10 @@ var RunnerServer = class {
134956
135024
  }
134957
135025
  handleRunTurn(client, raw) {
134958
135026
  const params = runTurnParamsSchema.parse(raw);
134959
- const turnId = newTurnId();
135027
+ const turnId = params.turnId ? asTurnId(params.turnId) : newTurnId();
135028
+ if (this.turnControllers.has(turnId)) {
135029
+ throw new Error(`turn id ${turnId} is already in flight \u2014 client-supplied turn ids must be unique`);
135030
+ }
134960
135031
  const controller = new AbortController();
134961
135032
  this.turnControllers.set(turnId, { controller, owner: client });
134962
135033
  client.turns.add(turnId);
@@ -135361,6 +135432,10 @@ var RemoteSession = class {
135361
135432
  this.peer.on(RunnerNotification.InfoChanged, (params) => {
135362
135433
  this.info = params.info;
135363
135434
  });
135435
+ this.peer.on(RunnerNotification.ReplayStart, (params) => {
135436
+ const { fromSeq } = params;
135437
+ this.mirror.rebase(fromSeq);
135438
+ });
135364
135439
  this.peer.on(RunnerNotification.SessionReset, () => {
135365
135440
  this.mirror.clear();
135366
135441
  });
@@ -135404,11 +135479,12 @@ var RemoteSession = class {
135404
135479
  return new Set(this.info?.readyProviders ?? []);
135405
135480
  }
135406
135481
  /** Handshake. Resolves once history has been replayed into the mirror. */
135407
- async attach(role, sinceSeq) {
135482
+ async attach(role, sinceSeq, replay) {
135408
135483
  const result = await this.peer.request(RunnerMethod.Attach, {
135409
135484
  protocolVersion: RUNNER_PROTOCOL_VERSION,
135410
135485
  role,
135411
- sinceSeq
135486
+ sinceSeq,
135487
+ ...replay ? { replay } : {}
135412
135488
  });
135413
135489
  this.info = result.info;
135414
135490
  this.serverProtocolVersion = typeof result.protocolVersion === "number" ? result.protocolVersion : RUNNER_PROTOCOL_VERSION;
@@ -135463,7 +135539,11 @@ var RemoteSession = class {
135463
135539
  ...opts.model ? { model: opts.model } : {},
135464
135540
  ...opts.systemPrompt ? { systemPrompt: opts.systemPrompt } : {},
135465
135541
  ...opts.maxIterations ? { maxIterations: opts.maxIterations } : {},
135466
- ...opts.attachments && opts.attachments.length > 0 ? { attachments: opts.attachments } : {}
135542
+ ...opts.attachments && opts.attachments.length > 0 ? { attachments: opts.attachments } : {},
135543
+ // Pre-minted client turn id (v6) so per-turn event filters match. The
135544
+ // reply's turnId stays authoritative: an older server ignores ours and
135545
+ // mints its own.
135546
+ ...opts.turnId ? { turnId: opts.turnId } : {}
135467
135547
  });
135468
135548
  const turnId = result.turnId;
135469
135549
  const stream = new TurnStream();
@@ -135758,7 +135838,7 @@ async function connectRemoteSession(opts = {}) {
135758
135838
  const transport = opts.transport ?? await connectWithRetry(socketPath, opts.connectRetries ?? 5);
135759
135839
  const session = new RemoteSession(transport);
135760
135840
  try {
135761
- await session.attach(opts.role ?? "client", opts.sinceSeq ?? 0);
135841
+ await session.attach(opts.role ?? "client", opts.sinceSeq ?? 0, opts.replay);
135762
135842
  return session;
135763
135843
  } catch (err) {
135764
135844
  await maybeRecoverFromMismatch(err, socketPath, opts);
@@ -137300,7 +137380,7 @@ var agentSpecSchema = z$1.object({
137300
137380
  agentType: z$1.string().optional().describe('Named agent kind to spawn (e.g. "researcher", "code-reviewer"). Looked up in the agent registry contributed by installed plugins. Omit, or pass "default", for a generic tool-use agent. Unknown types fall back to default \u2014 the request never fails over a missing kind. List of currently-registered kinds is visible via the /agents command.'),
137301
137381
  label: z$1.string().max(60).optional().describe('Short label shown in progress events (e.g. "research-deps", "lint-fix-A").'),
137302
137382
  systemPrompt: z$1.string().optional().describe("Override the kind's system prompt. Use to set persona, constraints, or hand off upstream artifacts the child needs as context."),
137303
- model: z$1.string().optional().describe("Model id override; defaults to the kind's model, then the parent's."),
137383
+ model: z$1.string().optional().describe("Model id override; defaults to the kind's model, then the parent's. Omit unless the user explicitly requested a specific model \u2014 do NOT invent model ids. Unknown ids fall back to the parent's model with a warning."),
137304
137384
  mode: z$1.string().optional().describe(`Loop strategy override. Valid values: "default", "goal", "research". OMIT for the kind's default \u2014 do NOT invent names.`),
137305
137385
  allowedTools: z$1.array(z$1.string()).optional().describe("Restrict the child to these tool names. Overrides the kind's allowlist if set.")
137306
137386
  });
@@ -142603,16 +142683,54 @@ function findCycle(steps) {
142603
142683
  }
142604
142684
  return null;
142605
142685
  }
142606
- function formatIssues(error2) {
142686
+ function humanizeIssue(iss) {
142687
+ switch (iss.code) {
142688
+ case "too_small": {
142689
+ const min = Number(iss.minimum);
142690
+ if (iss.type === "string")
142691
+ return min === 1 ? "must not be empty" : `must be at least ${min} characters`;
142692
+ if (iss.type === "array")
142693
+ return min === 1 ? "needs at least one entry" : `needs at least ${min} entries`;
142694
+ return `must be at least ${min}`;
142695
+ }
142696
+ case "too_big": {
142697
+ const max = Number(iss.maximum);
142698
+ if (iss.type === "string")
142699
+ return `must be at most ${max} characters`;
142700
+ if (iss.type === "array")
142701
+ return `can have at most ${max} entries`;
142702
+ return `must be at most ${max}`;
142703
+ }
142704
+ case "invalid_type":
142705
+ return iss.received === "undefined" ? "is required" : `should be a ${iss.expected} (got ${iss.received})`;
142706
+ case "invalid_enum_value":
142707
+ return `must be one of: ${iss.options.map((o2) => `"${String(o2)}"`).join(", ")}`;
142708
+ default:
142709
+ return iss.message;
142710
+ }
142711
+ }
142712
+ function formatIssues(error2, raw) {
142713
+ const steps = raw !== null && typeof raw === "object" && Array.isArray(raw.steps) ? raw.steps : [];
142607
142714
  return error2.issues.map((iss) => {
142608
- const path61 = iss.path.join(".") || "(root)";
142609
- return `${path61}: ${iss.message}`;
142715
+ if (iss.code === "custom")
142716
+ return iss.message;
142717
+ const msg = humanizeIssue(iss);
142718
+ const [head, idx, ...rest] = iss.path;
142719
+ if (head === "steps" && typeof idx === "number") {
142720
+ const step = steps[idx];
142721
+ const id = step !== null && typeof step === "object" && typeof step.id === "string" ? step.id : "";
142722
+ const where = id ? `step "${id}"` : `step ${idx + 1}`;
142723
+ const field = rest.join(".");
142724
+ return field ? `${where}: ${field} ${msg}` : `${where} ${msg}`;
142725
+ }
142726
+ const path61 = iss.path.join(".");
142727
+ return path61 ? `${path61} ${msg}` : `workflow ${msg}`;
142610
142728
  });
142611
142729
  }
142612
142730
  function validateWorkflow(raw) {
142613
142731
  const parsed = workflowSchema.safeParse(raw);
142614
142732
  if (!parsed.success)
142615
- return { ok: false, errors: formatIssues(parsed.error) };
142733
+ return { ok: false, errors: formatIssues(parsed.error, raw) };
142616
142734
  return { ok: true, workflow: parsed.data, errors: [] };
142617
142735
  }
142618
142736
  function parseWorkflowYaml(text) {
@@ -143918,7 +144036,7 @@ function createTool(deps) {
143918
144036
  if (!provider) {
143919
144037
  throw new MoxxyError({ code: "PROVIDER_NOT_CONFIGURED", message: "workflow_create: no active provider to draft with." });
143920
144038
  }
143921
- const model = deps.draftModel ?? provider.models[0]?.id ?? "claude-sonnet-4-6";
144039
+ const model = deps.draftModel ?? provider.models[0]?.id ?? "default";
143922
144040
  const drafted = await draftWorkflow(provider, model, intent, ctx.signal, {
143923
144041
  ...deps.listSkills ? { availableSkills: deps.listSkills() } : {},
143924
144042
  ...deps.listTools ? { availableTools: deps.listTools() } : {},
@@ -144809,7 +144927,7 @@ async function fireAfterWorkflowDependents(args) {
144809
144927
  }
144810
144928
  }
144811
144929
  function activeModel(session) {
144812
- return safeActiveProvider(session)?.models[0]?.id ?? "claude-sonnet-4-6";
144930
+ return session.lastResolvedModel ?? safeActiveProvider(session)?.models[0]?.id ?? "default";
144813
144931
  }
144814
144932
  function safeActiveProvider(session) {
144815
144933
  try {
@@ -145318,6 +145436,15 @@ function scopedSessionView(session, allowedTools, triggerName) {
145318
145436
  get pluginHost() {
145319
145437
  return session.pluginHost;
145320
145438
  },
145439
+ // Read-through AND write-through: runTurn records the resolved model on
145440
+ // the session it was handed, and that must land on the real session, not
145441
+ // this per-fire view.
145442
+ get lastResolvedModel() {
145443
+ return session.lastResolvedModel;
145444
+ },
145445
+ set lastResolvedModel(model) {
145446
+ session.lastResolvedModel = model;
145447
+ },
145321
145448
  startTurn: () => session.startTurn(),
145322
145449
  appContext: () => session.appContext()
145323
145450
  };