@runtypelabs/cli 2.18.0 → 2.19.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.
Files changed (3) hide show
  1. package/README.md +28 -1
  2. package/dist/index.js +257 -122
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -211,7 +211,7 @@ milestones:
211
211
  EOF
212
212
  ```
213
213
 
214
- **Search order**: Exact path → `.runtype/marathons/playbooks/<name>.yaml|yml|json` (repo) → `~/.runtype/marathons/playbooks/<name>.yaml|yml|json` (user).
214
+ **Search order**: Exact path → `.runtype/marathons/playbooks/<name>.yaml|yml|json|ts|mts` (repo) → `~/.runtype/marathons/playbooks/<name>.yaml|yml|json|ts|mts` (user).
215
215
 
216
216
  **Completion criteria types**:
217
217
 
@@ -248,6 +248,33 @@ milestones:
248
248
  | `requireVerification` | `boolean` | Require verification before `TASK_COMPLETE`. |
249
249
  | `outputRoot` | `string` | For creation tasks: confine writes to this directory (e.g. `"public/"`). |
250
250
 
251
+ #### TypeScript playbooks
252
+
253
+ Playbooks can also be TypeScript modules (`.ts`/`.mts`), loaded at runtime via jiti — no build step or special Node version required. Every behavior slot (`instructions`, `completionCriteria`, `recovery`, `intercept`, ...) then accepts a plain function in addition to inline data and hook references:
254
+
255
+ ```ts
256
+ // .runtype/marathons/playbooks/my-task.ts
257
+ import { definePlaybook, type RunTaskStateSlice } from '@runtypelabs/sdk'
258
+
259
+ export default definePlaybook({
260
+ name: 'my-task',
261
+ stallPolicy: { nudgeAfter: 1, stopAfter: 4 },
262
+ milestones: [
263
+ {
264
+ name: 'build',
265
+ instructions: (state: RunTaskStateSlice) => `Build it. Plan: ${state.planPath}`,
266
+ recovery: (state) =>
267
+ `You went ${state.consecutiveEmptySessions ?? 0} sessions without a tool call. Write a file now.`,
268
+ canAcceptCompletion: true,
269
+ },
270
+ ],
271
+ })
272
+ ```
273
+
274
+ `definePlaybook` (from `@runtypelabs/sdk`, install as a devDependency for editor types) is optional sugar — a plain object export with the same shape works without the package installed. To register named hooks (reusable from YAML playbooks too), export a factory instead: `export default ({ registerWorkflowHook }) => ({ ... })`. A complete example lives at [`examples/playbooks/release-notes.ts`](./examples/playbooks/release-notes.ts).
275
+
276
+ **Hook references**: any slot can reference a registered behavior by name instead of carrying data — `builtin:*` names expose the default workflow's behaviors (e.g. `instructions: builtin:research-instructions`, `completionCriteria: { type: builtin:research-complete }`), and YAML playbooks can load custom hooks from JS modules listed under `plugins:` (paths relative to the playbook file). See the comments in [`examples/playbooks/design-library.yaml`](./examples/playbooks/design-library.yaml).
277
+
251
278
  #### Marathon Anatomy
252
279
 
253
280
  ```
package/dist/index.js CHANGED
@@ -7,8 +7,13 @@ var __require = /* @__PURE__ */ ((x2) => typeof require !== "undefined" ? requir
7
7
  if (typeof require !== "undefined") return require.apply(this, arguments);
8
8
  throw Error('Dynamic require of "' + x2 + '" is not supported');
9
9
  });
10
- var __esm = (fn, res) => function __init() {
11
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ var __esm = (fn, res, err) => function __init() {
11
+ if (err) throw err[0];
12
+ try {
13
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ } catch (e) {
15
+ throw err = [e], e;
16
+ }
12
17
  };
13
18
  var __export = (target, all) => {
14
19
  for (var name in all)
@@ -33515,7 +33520,11 @@ var userProfileFeaturesSchema = external_exports.object({
33515
33520
  enableDashboardAssistant: external_exports.boolean(),
33516
33521
  // Routed model id the dashboard assistant dispatches with. Driven by the
33517
33522
  // `dashboard-assistant-model` string flag.
33518
- dashboardAssistantModel: external_exports.string()
33523
+ dashboardAssistantModel: external_exports.string(),
33524
+ // Gates the chrome_extension surface type in the dashboard (surface picker
33525
+ // and downstream config/ship UI). Driven by the `CHROME_SURFACE` boolean
33526
+ // flag.
33527
+ enableChromeSurface: external_exports.boolean()
33519
33528
  });
33520
33529
  var MODEL_FAMILY_PROVIDER_IDS = {
33521
33530
  "claude-fable-5": {
@@ -36482,7 +36491,8 @@ var surfaceSchema = external_exports.object({
36482
36491
  "messaging",
36483
36492
  "telegram",
36484
36493
  "a2a",
36485
- "hosted-page"
36494
+ "hosted-page",
36495
+ "chrome_extension"
36486
36496
  ]),
36487
36497
  config: external_exports.record(external_exports.string(), external_exports.any()),
36488
36498
  createPolicy: createPolicySchema,
@@ -37946,7 +37956,8 @@ var PLATFORM_CATALOG = {
37946
37956
  "whatsapp",
37947
37957
  "messaging",
37948
37958
  "telegram",
37949
- "a2a"
37959
+ "a2a",
37960
+ "chrome_extension"
37950
37961
  ],
37951
37962
  availableFlowPrimitives: FLOW_STEP_TYPES.map((stepType) => {
37952
37963
  const meta3 = FLOW_STEP_TYPE_METADATA[stepType];
@@ -38336,6 +38347,37 @@ var SURFACE_TYPE_METADATA = {
38336
38347
  maxResponseLength: null,
38337
38348
  executionHint: null
38338
38349
  }
38350
+ },
38351
+ // @snake-case-ok: surface type identifier
38352
+ chrome_extension: {
38353
+ description: "Downloadable Manifest V3 Chrome extension embedding the agent in the browser side panel, with packaged browser tools (read page, fill forms, navigate tabs) executed locally via the WebMCP client-tool loop.",
38354
+ useCases: [
38355
+ "browser copilots",
38356
+ "page-aware assistants",
38357
+ "form filling and data extraction",
38358
+ "internal browser tooling"
38359
+ ],
38360
+ examples: [
38361
+ "CRM sidekick that reads and updates records on the page",
38362
+ "Research assistant that summarizes and compares open tabs",
38363
+ "Support agent that drafts replies inside a ticketing UI"
38364
+ ],
38365
+ traits: {
38366
+ streaming: "required",
38367
+ messagesMutable: false,
38368
+ deliveryModel: "real_time",
38369
+ mediaSupport: "images",
38370
+ interactiveUI: "generative",
38371
+ inboundMediaSupport: true,
38372
+ consumerType: "human",
38373
+ reasoningVisibility: "pass_through",
38374
+ markdownDialect: "mdx",
38375
+ threadModel: "flat",
38376
+ senderIdentity: "anonymous",
38377
+ maxResponseLength: null,
38378
+ executionHint: "You are running inside a Chrome extension side panel. You may have browser tools (chrome:*) to read the current page, interact with forms, and manage tabs \u2014 use them when the user asks about or wants to act on the page they are viewing. Mutating actions (clicking, filling, navigating) ask the user for confirmation before running."
38379
+ },
38380
+ behaviorTypeRef: "runtype://types/surface-configs"
38339
38381
  }
38340
38382
  };
38341
38383
  var SURFACE_TYPE_GUIDE = (() => {
@@ -55751,14 +55793,17 @@ function resolveErrorHandlingForPhase(phase, cliFallbackModel, milestoneFallback
55751
55793
  import * as fs14 from "fs";
55752
55794
  import * as path15 from "path";
55753
55795
  import * as os5 from "os";
55796
+ import { pathToFileURL } from "url";
55797
+ import { createJiti } from "jiti";
55754
55798
  import micromatch from "micromatch";
55755
55799
  import { parse as parseYaml } from "yaml";
55756
- var DISCOVERY_TOOLS = /* @__PURE__ */ new Set([
55757
- "search_repo",
55758
- "glob_files",
55759
- "tree_directory",
55760
- "list_directory"
55761
- ]);
55800
+ import {
55801
+ DEFAULT_STALL_STOP_AFTER,
55802
+ compileWorkflowConfig,
55803
+ ensureDefaultWorkflowHooks,
55804
+ isWorkflowHookRef,
55805
+ registerWorkflowHook
55806
+ } from "@runtypelabs/sdk";
55762
55807
  var PLAYBOOKS_DIR = ".runtype/marathons/playbooks";
55763
55808
  function getCandidatePaths(nameOrPath, cwd) {
55764
55809
  const home = os5.homedir();
@@ -55769,12 +55814,43 @@ function getCandidatePaths(nameOrPath, cwd) {
55769
55814
  path15.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.yaml`),
55770
55815
  path15.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.yml`),
55771
55816
  path15.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.json`),
55817
+ path15.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.ts`),
55818
+ path15.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.mts`),
55772
55819
  // User-level
55773
55820
  path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.yaml`),
55774
55821
  path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.yml`),
55775
- path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.json`)
55822
+ path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.json`),
55823
+ path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.ts`),
55824
+ path15.resolve(home, PLAYBOOKS_DIR, `${nameOrPath}.mts`)
55776
55825
  ];
55777
55826
  }
55827
+ var MODULE_PLAYBOOK_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mts"]);
55828
+ var jitiLoader;
55829
+ async function loadModulePlaybook(filePath) {
55830
+ jitiLoader ??= createJiti(import.meta.url, { interopDefault: false });
55831
+ let mod;
55832
+ try {
55833
+ mod = await jitiLoader.import(filePath);
55834
+ } catch (error51) {
55835
+ const message = error51 instanceof Error ? error51.message : String(error51);
55836
+ throw new Error(`Failed to load TypeScript playbook at ${filePath}: ${message}`, {
55837
+ cause: error51
55838
+ });
55839
+ }
55840
+ const exported = mod.default;
55841
+ if (exported === void 0 || exported === null) {
55842
+ throw new Error(
55843
+ `TypeScript playbook at ${filePath} must have a default export: a playbook config object, or a factory function receiving { registerWorkflowHook }. Wrap it in definePlaybook(...) from @runtypelabs/sdk for type inference.`
55844
+ );
55845
+ }
55846
+ const config3 = typeof exported === "function" ? await exported({ registerWorkflowHook }) : exported;
55847
+ if (!config3 || typeof config3 !== "object" || Array.isArray(config3)) {
55848
+ throw new Error(
55849
+ `TypeScript playbook at ${filePath} did not produce a playbook config object.`
55850
+ );
55851
+ }
55852
+ return config3;
55853
+ }
55778
55854
  function parsePlaybookFile(filePath) {
55779
55855
  const raw = fs14.readFileSync(filePath, "utf-8");
55780
55856
  const ext = path15.extname(filePath).toLowerCase();
@@ -55783,6 +55859,12 @@ function parsePlaybookFile(filePath) {
55783
55859
  }
55784
55860
  return parseYaml(raw);
55785
55861
  }
55862
+ function assertPositiveInteger(value, label, playbookName) {
55863
+ if (value === void 0) return;
55864
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
55865
+ throw new Error(`Playbook '${playbookName}': ${label} must be a positive integer`);
55866
+ }
55867
+ }
55786
55868
  function validatePlaybook(config3, filePath) {
55787
55869
  if (!config3.name) {
55788
55870
  throw new Error(`Playbook at ${filePath} is missing required 'name' field`);
@@ -55797,131 +55879,140 @@ function validatePlaybook(config3, filePath) {
55797
55879
  if (!milestone.instructions) {
55798
55880
  throw new Error(`Playbook '${config3.name}': milestone '${milestone.name}' is missing 'instructions'`);
55799
55881
  }
55882
+ if (milestone.recovery !== void 0 && typeof milestone.recovery !== "function" && !isWorkflowHookRef(milestone.recovery)) {
55883
+ const recovery = milestone.recovery;
55884
+ if (typeof recovery.message !== "string" || !recovery.message.trim()) {
55885
+ throw new Error(
55886
+ `Playbook '${config3.name}': milestone '${milestone.name}' recovery is missing 'message'`
55887
+ );
55888
+ }
55889
+ assertPositiveInteger(
55890
+ recovery.afterEmptySessions,
55891
+ `milestone '${milestone.name}' recovery.afterEmptySessions`,
55892
+ config3.name
55893
+ );
55894
+ }
55800
55895
  }
55801
- }
55802
- function interpolate(template, state) {
55803
- return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
55804
- const value = state[key];
55805
- if (value === void 0 || value === null) return `{{${key}}}`;
55806
- return String(value);
55807
- });
55808
- }
55809
- function buildIsComplete(criteria) {
55810
- if (!criteria) return () => false;
55811
- switch (criteria.type) {
55812
- case "evidence":
55813
- return (ctx) => {
55814
- const minFiles = criteria.minReadFiles ?? 1;
55815
- return (ctx.state.recentReadPaths?.length ?? 0) >= minFiles;
55816
- };
55817
- case "sessions": {
55818
- let baselineSessionCount;
55819
- return (ctx) => {
55820
- const minSessions = criteria.minSessions ?? 1;
55821
- if (baselineSessionCount === void 0) {
55822
- baselineSessionCount = ctx.state.sessions.length;
55823
- }
55824
- return ctx.state.sessions.length - baselineSessionCount >= minSessions;
55825
- };
55896
+ if (config3.stallPolicy) {
55897
+ assertPositiveInteger(config3.stallPolicy.nudgeAfter, "stallPolicy.nudgeAfter", config3.name);
55898
+ assertPositiveInteger(
55899
+ config3.stallPolicy.escalateModelAfter,
55900
+ "stallPolicy.escalateModelAfter",
55901
+ config3.name
55902
+ );
55903
+ assertPositiveInteger(config3.stallPolicy.stopAfter, "stallPolicy.stopAfter", config3.name);
55904
+ }
55905
+ if (config3.plugins !== void 0) {
55906
+ if (!Array.isArray(config3.plugins)) {
55907
+ throw new Error(`Playbook '${config3.name}': 'plugins' must be a list of relative module paths`);
55908
+ }
55909
+ for (const plugin of config3.plugins) {
55910
+ if (typeof plugin !== "string" || !plugin.trim()) {
55911
+ throw new Error(`Playbook '${config3.name}': each plugins entry must be a relative module path`);
55912
+ }
55826
55913
  }
55827
- case "planWritten":
55828
- return (ctx) => {
55829
- return ctx.trace.planWritten;
55830
- };
55831
- case "never":
55832
- default:
55833
- return () => false;
55834
55914
  }
55835
55915
  }
55836
- function buildPolicyIntercept(policy) {
55837
- if (!policy.blockedTools?.length && !policy.blockDiscoveryTools && !policy.allowedReadGlobs?.length && !policy.allowedWriteGlobs?.length && !policy.requirePlanBeforeWrite) {
55838
- return void 0;
55839
- }
55840
- const blockedSet = new Set(
55841
- (policy.blockedTools ?? []).map((t) => t.trim()).filter(Boolean)
55916
+ function collectPlaybookWarnings(config3) {
55917
+ const warnings = [];
55918
+ const anyCompletable = config3.milestones.some(
55919
+ (m2) => m2.canAcceptCompletion === true || typeof m2.canAcceptCompletion === "function" || isWorkflowHookRef(m2.canAcceptCompletion)
55842
55920
  );
55843
- const readGlobs = policy.allowedReadGlobs ?? [];
55844
- const writeGlobs = policy.allowedWriteGlobs ?? [];
55845
- return (toolName, args, ctx) => {
55846
- if (blockedSet.has(toolName)) {
55847
- return `Blocked by playbook policy: ${toolName} is not allowed for this task.`;
55848
- }
55849
- if (policy.blockDiscoveryTools && DISCOVERY_TOOLS.has(toolName)) {
55850
- return `Blocked by playbook policy: discovery tools are disabled for this task.`;
55851
- }
55852
- const pathArg = typeof args.path === "string" && args.path.trim() ? ctx.normalizePath(String(args.path)) : void 0;
55853
- if (pathArg) {
55854
- const isWrite = toolName === "write_file" || toolName === "restore_file_checkpoint";
55855
- const isRead = toolName === "read_file";
55856
- if (isRead && readGlobs.length > 0) {
55857
- const allowed = micromatch.some(pathArg, readGlobs, { dot: true });
55858
- if (!allowed) {
55859
- return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed read globs: ${readGlobs.join(", ")}`;
55860
- }
55861
- }
55862
- if (isWrite && writeGlobs.length > 0) {
55863
- const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
55864
- if (planPath && pathArg === planPath) {
55865
- } else {
55866
- const allowed = micromatch.some(pathArg, writeGlobs, { dot: true });
55867
- if (!allowed) {
55868
- return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed write globs: ${writeGlobs.join(", ")}`;
55869
- }
55870
- }
55871
- }
55872
- if (isWrite && policy.requirePlanBeforeWrite && !ctx.state.planWritten && !ctx.trace.planWritten) {
55873
- const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
55874
- if (!planPath || pathArg !== planPath) {
55875
- return `Blocked by playbook policy: write the plan before creating other files.`;
55876
- }
55877
- }
55921
+ if (!anyCompletable) {
55922
+ warnings.push(
55923
+ `Playbook '${config3.name}': no milestone sets canAcceptCompletion: true \u2014 TASK_COMPLETE will never be accepted and the run can only end by stalling or exhausting its budget. Set it on the final milestone.`
55924
+ );
55925
+ }
55926
+ const escalateAfter = config3.stallPolicy?.escalateModelAfter;
55927
+ if (escalateAfter !== void 0) {
55928
+ const anyFallbacks = config3.milestones.some((m2) => m2.fallbackModels?.length);
55929
+ if (!anyFallbacks) {
55930
+ warnings.push(
55931
+ `Playbook '${config3.name}': stallPolicy.escalateModelAfter is set but no milestone defines fallbackModels \u2014 the escalation signal will have no model to switch to.`
55932
+ );
55878
55933
  }
55879
- return void 0;
55880
- };
55934
+ const stopAfter = config3.stallPolicy?.stopAfter ?? DEFAULT_STALL_STOP_AFTER;
55935
+ if (escalateAfter >= stopAfter) {
55936
+ warnings.push(
55937
+ `Playbook '${config3.name}': stallPolicy.escalateModelAfter (${escalateAfter}) is not below stopAfter (${stopAfter}) \u2014 the run will stall before model escalation can fire.`
55938
+ );
55939
+ }
55940
+ }
55941
+ return warnings;
55881
55942
  }
55882
- function convertToWorkflow(config3) {
55883
- const policyIntercept = config3.policy ? buildPolicyIntercept(config3.policy) : void 0;
55884
- const phases = config3.milestones.map((milestone) => ({
55943
+ var PLUGIN_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs"]);
55944
+ async function loadPlaybookPlugins(plugins, playbookFilePath, playbookName) {
55945
+ if (!plugins?.length) return;
55946
+ const baseDir = path15.dirname(playbookFilePath);
55947
+ for (const plugin of plugins) {
55948
+ if (path15.isAbsolute(plugin)) {
55949
+ throw new Error(
55950
+ `Playbook '${playbookName}': plugin "${plugin}" must be a relative path (resolved against the playbook's directory).`
55951
+ );
55952
+ }
55953
+ const resolved = path15.resolve(baseDir, plugin);
55954
+ if (resolved !== baseDir && !resolved.startsWith(baseDir + path15.sep)) {
55955
+ throw new Error(
55956
+ `Playbook '${playbookName}': plugin "${plugin}" resolves outside the playbook's directory. Keep plugin modules next to the playbook.`
55957
+ );
55958
+ }
55959
+ if (!PLUGIN_EXTENSIONS.has(path15.extname(resolved).toLowerCase())) {
55960
+ throw new Error(
55961
+ `Playbook '${playbookName}': plugin "${plugin}" must be a .js, .mjs, or .cjs module.`
55962
+ );
55963
+ }
55964
+ if (!fs14.existsSync(resolved)) {
55965
+ throw new Error(`Playbook '${playbookName}': plugin "${plugin}" not found at ${resolved}.`);
55966
+ }
55967
+ const mod = await import(pathToFileURL(resolved).href);
55968
+ if (typeof mod.default !== "function") {
55969
+ throw new Error(
55970
+ `Playbook '${playbookName}': plugin "${plugin}" must export a default function: export default ({ registerWorkflowHook }) => { ... }`
55971
+ );
55972
+ }
55973
+ await mod.default({ registerWorkflowHook });
55974
+ }
55975
+ }
55976
+ function toWorkflowConfig(config3) {
55977
+ const milestones = config3.milestones.map((milestone) => ({
55885
55978
  name: milestone.name,
55886
- description: milestone.description,
55887
- buildInstructions(state) {
55888
- const header = `--- Workflow Phase: ${milestone.name} ---`;
55889
- const desc = milestone.description ? `
55890
- ${milestone.description}` : "";
55891
- const instructions = interpolate(milestone.instructions, state);
55892
- return `${header}${desc}
55893
- ${instructions}`;
55894
- },
55895
- buildToolGuidance(_state) {
55896
- return milestone.toolGuidance ?? [];
55897
- },
55898
- isComplete: buildIsComplete(milestone.completionCriteria),
55899
- interceptToolCall: policyIntercept,
55900
- // Default to rejecting TASK_COMPLETE unless the playbook explicitly allows it.
55901
- // The SDK accepts completion by default when canAcceptCompletion is undefined,
55902
- // which would let the model end the marathon prematurely in early phases.
55903
- canAcceptCompletion: milestone.canAcceptCompletion !== void 0 ? () => milestone.canAcceptCompletion : () => false
55979
+ ...milestone.description !== void 0 ? { description: milestone.description } : {},
55980
+ instructions: milestone.instructions,
55981
+ ...milestone.toolGuidance !== void 0 ? { toolGuidance: milestone.toolGuidance } : {},
55982
+ ...milestone.completionCriteria ? { completionCriteria: milestone.completionCriteria } : {},
55983
+ ...milestone.recovery !== void 0 ? { recovery: milestone.recovery } : {},
55984
+ ...milestone.transitionSummary !== void 0 ? { transitionSummary: milestone.transitionSummary } : {},
55985
+ ...milestone.intercept ? { intercept: milestone.intercept } : {},
55986
+ ...milestone.forceEndTurn ? { forceEndTurn: milestone.forceEndTurn } : {},
55987
+ // Playbooks reject TASK_COMPLETE unless explicitly allowed — the SDK
55988
+ // accepts completion when the slot is absent, which would let the model
55989
+ // end the marathon prematurely in early milestones.
55990
+ canAcceptCompletion: milestone.canAcceptCompletion ?? false
55904
55991
  }));
55905
55992
  return {
55906
55993
  name: config3.name,
55907
- phases
55908
- };
55909
- }
55910
- function normalizeFallbackModel(input) {
55911
- if (typeof input === "string") return { model: input };
55912
- return {
55913
- model: input.model,
55914
- ...input.temperature !== void 0 ? { temperature: input.temperature } : {},
55915
- ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {}
55994
+ ...config3.description !== void 0 ? { description: config3.description } : {},
55995
+ ...config3.stallPolicy ? { stallPolicy: config3.stallPolicy } : {},
55996
+ ...config3.policy ? { policy: config3.policy } : {},
55997
+ ...config3.classifyVariant ? { classifyVariant: config3.classifyVariant } : {},
55998
+ ...config3.bootstrap ? { bootstrap: config3.bootstrap } : {},
55999
+ ...config3.candidateBlock ? { candidateBlock: config3.candidateBlock } : {},
56000
+ milestones
55916
56001
  };
55917
56002
  }
55918
- function loadPlaybook(nameOrPath, cwd) {
56003
+ async function loadPlaybook(nameOrPath, cwd) {
55919
56004
  const baseCwd = cwd || process.cwd();
55920
56005
  const candidates = getCandidatePaths(nameOrPath, baseCwd);
55921
56006
  for (const candidate of candidates) {
55922
56007
  if (!fs14.existsSync(candidate) || fs14.statSync(candidate).isDirectory()) continue;
55923
- const config3 = parsePlaybookFile(candidate);
56008
+ const ext = path15.extname(candidate).toLowerCase();
56009
+ const config3 = MODULE_PLAYBOOK_EXTENSIONS.has(ext) ? await loadModulePlaybook(candidate) : parsePlaybookFile(candidate);
55924
56010
  validatePlaybook(config3, candidate);
56011
+ await loadPlaybookPlugins(config3.plugins, candidate, config3.name);
56012
+ ensureDefaultWorkflowHooks();
56013
+ const workflow = compileWorkflowConfig(toWorkflowConfig(config3), {
56014
+ matchPathGlobs: (filePath, globs) => micromatch.some(filePath, globs, { dot: true })
56015
+ });
55925
56016
  const milestoneModels = {};
55926
56017
  const milestoneFallbackModels = {};
55927
56018
  for (const m2 of config3.milestones) {
@@ -55931,13 +56022,14 @@ function loadPlaybook(nameOrPath, cwd) {
55931
56022
  }
55932
56023
  }
55933
56024
  return {
55934
- workflow: convertToWorkflow(config3),
56025
+ workflow,
55935
56026
  milestones: config3.milestones.map((m2) => m2.name),
55936
56027
  milestoneModels: Object.keys(milestoneModels).length > 0 ? milestoneModels : void 0,
55937
56028
  milestoneFallbackModels: Object.keys(milestoneFallbackModels).length > 0 ? milestoneFallbackModels : void 0,
55938
56029
  verification: config3.verification,
55939
56030
  rules: config3.rules,
55940
- policy: config3.policy
56031
+ policy: config3.policy,
56032
+ warnings: collectPlaybookWarnings(config3)
55941
56033
  };
55942
56034
  }
55943
56035
  throw new Error(
@@ -55945,6 +56037,14 @@ function loadPlaybook(nameOrPath, cwd) {
55945
56037
  ${candidates.map((c2) => ` ${c2}`).join("\n")}`
55946
56038
  );
55947
56039
  }
56040
+ function normalizeFallbackModel(input) {
56041
+ if (typeof input === "string") return { model: input };
56042
+ return {
56043
+ model: input.model,
56044
+ ...input.temperature !== void 0 ? { temperature: input.temperature } : {},
56045
+ ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {}
56046
+ };
56047
+ }
55948
56048
 
55949
56049
  // src/commands/agents-task.ts
55950
56050
  function shouldRequestResumeCheckpoint(status, resumeMessage, noCheckpoint, originalMessage, continuations) {
@@ -56485,12 +56585,19 @@ async function taskAction(agent, options) {
56485
56585
  let playbookMilestoneFallbackModels;
56486
56586
  let playbookPolicy;
56487
56587
  if (options.playbook) {
56488
- const result = loadPlaybook(options.playbook);
56588
+ const result = await loadPlaybook(options.playbook);
56489
56589
  playbookWorkflow = result.workflow;
56490
56590
  playbookMilestones = result.milestones;
56491
56591
  playbookMilestoneModels = result.milestoneModels;
56492
56592
  playbookMilestoneFallbackModels = result.milestoneFallbackModels;
56493
56593
  playbookPolicy = result.policy;
56594
+ for (const warning of result.warnings) {
56595
+ if (useStartupShell) {
56596
+ setStartupStatus(`playbook warning: ${warning}`);
56597
+ } else {
56598
+ console.log(chalk23.yellow(`Playbook warning: ${warning}`));
56599
+ }
56600
+ }
56494
56601
  } else {
56495
56602
  playbookPolicy = void 0;
56496
56603
  }
@@ -56866,6 +56973,20 @@ Saving state... done. Session saved to ${filePath}`);
56866
56973
  break;
56867
56974
  }
56868
56975
  }
56976
+ const stallEscalationUsedModels = /* @__PURE__ */ new Map();
56977
+ const pickNextStallFallbackModel = (phase, currentModel) => {
56978
+ if (!phase) return void 0;
56979
+ const chain = [
56980
+ ...playbookMilestoneFallbackModels?.[phase]?.map((fb) => fb.model) ?? [],
56981
+ ...options.fallbackModel ? [options.fallbackModel] : []
56982
+ ];
56983
+ const used = stallEscalationUsedModels.get(phase) ?? /* @__PURE__ */ new Set();
56984
+ const next = chain.find((model) => model !== currentModel && !used.has(model));
56985
+ if (!next) return void 0;
56986
+ used.add(next);
56987
+ stallEscalationUsedModels.set(phase, used);
56988
+ return next;
56989
+ };
56869
56990
  let shouldContinue = true;
56870
56991
  let accumulatedSessions = 0;
56871
56992
  let accumulatedCost = 0;
@@ -57081,6 +57202,20 @@ Saving state... done. Session saved to ${filePath}`);
57081
57202
  });
57082
57203
  }
57083
57204
  }
57205
+ if (state.stallEscalationRequested && state.status === "running") {
57206
+ const escalationPhase = state.workflowPhase;
57207
+ const activeModel = phaseModel || options.model;
57208
+ const nextModel = pickNextStallFallbackModel(escalationPhase, activeModel);
57209
+ if (escalationPhase && nextModel) {
57210
+ playbookMilestoneModels = {
57211
+ ...playbookMilestoneModels ?? {},
57212
+ [escalationPhase]: nextModel
57213
+ };
57214
+ options.model = nextModel;
57215
+ shouldContinue = true;
57216
+ return false;
57217
+ }
57218
+ }
57084
57219
  if (state.recentActionKeys && state.recentActionKeys.length > 0) {
57085
57220
  for (const key of state.recentActionKeys) {
57086
57221
  loopDetector.recordAction(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/cli",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "Command-line interface for Runtype AI platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "ink": "6.7.0",
17
17
  "ink-select-input": "^6.2.0",
18
18
  "ink-text-input": "^6.0.0",
19
+ "jiti": "^2.7.0",
19
20
  "micromatch": "^4.0.8",
20
21
  "oauth4webapi": "^3.8.5",
21
22
  "open": "^10.1.0",
@@ -23,7 +24,7 @@
23
24
  "rosie-skills": "0.8.1",
24
25
  "yaml": "^2.9.0",
25
26
  "@runtypelabs/ink-components": "0.3.2",
26
- "@runtypelabs/sdk": "4.10.0",
27
+ "@runtypelabs/sdk": "4.11.0",
27
28
  "@runtypelabs/terminal-animations": "0.2.1"
28
29
  },
29
30
  "devDependencies": {
@@ -38,7 +39,7 @@
38
39
  "tsx": "^4.7.1",
39
40
  "typescript": "^5.3.3",
40
41
  "vitest": "^4.1.0",
41
- "@runtypelabs/shared": "1.23.0"
42
+ "@runtypelabs/shared": "1.24.0"
42
43
  },
43
44
  "engines": {
44
45
  "node": ">=22.0.0"