@runtypelabs/sdk 4.10.0 → 4.11.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/dist/index.mjs CHANGED
@@ -4310,6 +4310,261 @@ function sanitizeTaskSlug(taskName) {
4310
4310
  return taskName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
4311
4311
  }
4312
4312
 
4313
+ // src/workflows/hook-registry.ts
4314
+ var BUILTIN_NAMESPACE = "builtin";
4315
+ var HOOK_REF_PATTERN = /^[a-z0-9_-]+:[a-z0-9_-]+$/;
4316
+ function isWorkflowHookRef(value) {
4317
+ return typeof value === "string" && HOOK_REF_PATTERN.test(value);
4318
+ }
4319
+ var registry = /* @__PURE__ */ new Map();
4320
+ function registerWorkflowHook(name, entry) {
4321
+ if (!isWorkflowHookRef(name)) {
4322
+ throw new Error(
4323
+ `Invalid workflow hook name "${name}": must be "<namespace>:<id>" using lowercase letters, digits, "-" or "_" (e.g. "acme:my-completion").`
4324
+ );
4325
+ }
4326
+ if (name.startsWith(`${BUILTIN_NAMESPACE}:`)) {
4327
+ throw new Error(
4328
+ `Cannot register "${name}": the "builtin:" namespace is reserved. Register under your own namespace and reference it from the workflow config instead.`
4329
+ );
4330
+ }
4331
+ registry.set(name, entry);
4332
+ }
4333
+ function registerBuiltinWorkflowHook(name, entry) {
4334
+ if (!name.startsWith(`${BUILTIN_NAMESPACE}:`) || !isWorkflowHookRef(name)) {
4335
+ throw new Error(`Builtin workflow hooks must be named "builtin:<id>" (got "${name}").`);
4336
+ }
4337
+ if (registry.has(name)) return;
4338
+ registry.set(name, entry);
4339
+ }
4340
+ function resolveWorkflowHook(name, expectedKind) {
4341
+ const entry = registry.get(name);
4342
+ if (!entry) {
4343
+ const known = listWorkflowHooks().filter((hook) => hook.kind === expectedKind).map((hook) => hook.name);
4344
+ throw new Error(
4345
+ `Unknown workflow hook "${name}". ` + (known.length > 0 ? `Registered '${expectedKind}' hooks: ${known.join(", ")}.` : `No '${expectedKind}' hooks are registered.`) + " Custom hooks must be registered (e.g. via a playbook plugin) before the workflow is compiled."
4346
+ );
4347
+ }
4348
+ if (entry.kind !== expectedKind) {
4349
+ throw new Error(
4350
+ `Workflow hook "${name}" is registered as '${entry.kind}' but referenced from a '${expectedKind}' slot.`
4351
+ );
4352
+ }
4353
+ return entry.fn;
4354
+ }
4355
+ function listWorkflowHooks() {
4356
+ return [...registry.entries()].map(([name, entry]) => ({ name, kind: entry.kind }));
4357
+ }
4358
+ function unregisterWorkflowHook(name) {
4359
+ if (name.startsWith(`${BUILTIN_NAMESPACE}:`)) return false;
4360
+ return registry.delete(name);
4361
+ }
4362
+
4363
+ // src/workflows/workflow-config.ts
4364
+ var DISCOVERY_TOOLS = /* @__PURE__ */ new Set([
4365
+ "search_repo",
4366
+ "glob_files",
4367
+ "tree_directory",
4368
+ "list_directory"
4369
+ ]);
4370
+ var DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS = 2;
4371
+ function definePlaybook(playbook) {
4372
+ return playbook;
4373
+ }
4374
+ function interpolateWorkflowTemplate(template, state) {
4375
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
4376
+ const value = state[key];
4377
+ if (value === void 0 || value === null) return `{{${key}}}`;
4378
+ return String(value);
4379
+ });
4380
+ }
4381
+ function buildIsComplete(criteria, configName, milestoneName) {
4382
+ if (!criteria) return () => false;
4383
+ switch (criteria.type) {
4384
+ case "evidence":
4385
+ return (ctx) => {
4386
+ const minFiles = criteria.minReadFiles ?? 1;
4387
+ return (ctx.state.recentReadPaths?.length ?? 0) >= minFiles;
4388
+ };
4389
+ case "sessions": {
4390
+ let baselineSessionCount;
4391
+ return (ctx) => {
4392
+ const minSessions = criteria.minSessions ?? 1;
4393
+ if (baselineSessionCount === void 0) {
4394
+ baselineSessionCount = ctx.state.sessions.length;
4395
+ }
4396
+ return ctx.state.sessions.length - baselineSessionCount >= minSessions;
4397
+ };
4398
+ }
4399
+ case "planWritten":
4400
+ return (ctx) => {
4401
+ return ctx.trace.planWritten;
4402
+ };
4403
+ case "never":
4404
+ return () => false;
4405
+ default: {
4406
+ if (isWorkflowHookRef(criteria.type)) {
4407
+ return resolveWorkflowHook(criteria.type, "completion");
4408
+ }
4409
+ throw new Error(
4410
+ `Workflow config '${configName}': milestone '${milestoneName}' has unknown completionCriteria.type "${criteria.type}" (expected evidence | sessions | planWritten | never, or a 'completion' hook reference).`
4411
+ );
4412
+ }
4413
+ }
4414
+ }
4415
+ function buildPolicyIntercept(policy, configName, deps) {
4416
+ if (!policy.blockedTools?.length && !policy.blockDiscoveryTools && !policy.allowedReadGlobs?.length && !policy.allowedWriteGlobs?.length && !policy.requirePlanBeforeWrite) {
4417
+ return void 0;
4418
+ }
4419
+ const blockedSet = new Set(
4420
+ (policy.blockedTools ?? []).map((t) => t.trim()).filter(Boolean)
4421
+ );
4422
+ const readGlobs = policy.allowedReadGlobs ?? [];
4423
+ const writeGlobs = policy.allowedWriteGlobs ?? [];
4424
+ const matchPathGlobs = deps.matchPathGlobs;
4425
+ if ((readGlobs.length > 0 || writeGlobs.length > 0) && !matchPathGlobs) {
4426
+ throw new Error(
4427
+ `Workflow config '${configName}': policy uses allowedReadGlobs/allowedWriteGlobs but no glob matcher was provided to compileWorkflowConfig (pass deps.matchPathGlobs).`
4428
+ );
4429
+ }
4430
+ return (toolName, args, ctx) => {
4431
+ if (blockedSet.has(toolName)) {
4432
+ return `Blocked by playbook policy: ${toolName} is not allowed for this task.`;
4433
+ }
4434
+ if (policy.blockDiscoveryTools && DISCOVERY_TOOLS.has(toolName)) {
4435
+ return `Blocked by playbook policy: discovery tools are disabled for this task.`;
4436
+ }
4437
+ const pathArg = typeof args.path === "string" && args.path.trim() ? ctx.normalizePath(String(args.path)) : void 0;
4438
+ if (pathArg) {
4439
+ const isWrite = toolName === "write_file" || toolName === "restore_file_checkpoint";
4440
+ const isRead = toolName === "read_file";
4441
+ if (isRead && readGlobs.length > 0) {
4442
+ const allowed = matchPathGlobs(pathArg, readGlobs);
4443
+ if (!allowed) {
4444
+ return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed read globs: ${readGlobs.join(", ")}`;
4445
+ }
4446
+ }
4447
+ if (isWrite && writeGlobs.length > 0) {
4448
+ const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4449
+ if (planPath && pathArg === planPath) {
4450
+ } else {
4451
+ const allowed = matchPathGlobs(pathArg, writeGlobs);
4452
+ if (!allowed) {
4453
+ return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed write globs: ${writeGlobs.join(", ")}`;
4454
+ }
4455
+ }
4456
+ }
4457
+ if (isWrite && policy.requirePlanBeforeWrite && !ctx.state.planWritten && !ctx.trace.planWritten) {
4458
+ const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4459
+ if (!planPath || pathArg !== planPath) {
4460
+ return `Blocked by playbook policy: write the plan before creating other files.`;
4461
+ }
4462
+ }
4463
+ }
4464
+ return void 0;
4465
+ };
4466
+ }
4467
+ function buildPolicyGuidance(policy) {
4468
+ if (!policy) return [];
4469
+ const lines = [];
4470
+ if (policy.requirePlanBeforeWrite) {
4471
+ lines.push(
4472
+ "Policy: write the plan file before any other file. Once the plan is written, other writes are allowed in the same turn."
4473
+ );
4474
+ }
4475
+ if (policy.allowedWriteGlobs?.length) {
4476
+ lines.push(
4477
+ `Policy: file writes are only allowed for paths matching: ${policy.allowedWriteGlobs.join(", ")} (the plan file is always allowed).`
4478
+ );
4479
+ }
4480
+ if (policy.outputRoot) {
4481
+ lines.push(`Policy: create new files under "${policy.outputRoot.replace(/\/$/, "")}/".`);
4482
+ }
4483
+ if (policy.allowedReadGlobs?.length) {
4484
+ lines.push(
4485
+ `Policy: file reads are only allowed for paths matching: ${policy.allowedReadGlobs.join(", ")}.`
4486
+ );
4487
+ }
4488
+ if (policy.blockDiscoveryTools) {
4489
+ lines.push(
4490
+ "Policy: broad discovery tools (search_repo, glob_files, tree_directory, list_directory) are disabled for this task."
4491
+ );
4492
+ }
4493
+ if (policy.blockedTools?.length) {
4494
+ lines.push(`Policy: these tools are disabled for this task: ${policy.blockedTools.join(", ")}.`);
4495
+ }
4496
+ return lines;
4497
+ }
4498
+ function resolveSlotHook(value, kind) {
4499
+ if (value === void 0) return void 0;
4500
+ if (typeof value === "function") return value;
4501
+ return resolveWorkflowHook(value, kind);
4502
+ }
4503
+ function compileMilestone(milestone, config, policyIntercept, policyGuidance) {
4504
+ const buildInstructions = typeof milestone.instructions === "function" ? milestone.instructions : isWorkflowHookRef(milestone.instructions) ? resolveWorkflowHook(milestone.instructions, "instructions") : (state) => {
4505
+ const header = `--- Workflow Phase: ${milestone.name} ---`;
4506
+ const desc = milestone.description ? `
4507
+ ${milestone.description}` : "";
4508
+ const instructions = interpolateWorkflowTemplate(
4509
+ milestone.instructions,
4510
+ state
4511
+ );
4512
+ return `${header}${desc}
4513
+ ${instructions}`;
4514
+ };
4515
+ const guidanceHook = typeof milestone.toolGuidance === "function" ? milestone.toolGuidance : milestone.toolGuidance !== void 0 && isWorkflowHookRef(milestone.toolGuidance) ? resolveWorkflowHook(milestone.toolGuidance, "toolGuidance") : void 0;
4516
+ const buildToolGuidance = (state) => {
4517
+ const base = guidanceHook ? guidanceHook(state) : milestone.toolGuidance ?? [];
4518
+ return policyGuidance.length > 0 ? [...base, ...policyGuidance] : base;
4519
+ };
4520
+ const customIntercept = resolveSlotHook(milestone.intercept, "intercept");
4521
+ const interceptToolCall = policyIntercept && customIntercept ? (toolName, args, ctx) => policyIntercept(toolName, args, ctx) ?? customIntercept(toolName, args, ctx) : policyIntercept ?? customIntercept;
4522
+ const transitionHook = typeof milestone.transitionSummary === "function" ? milestone.transitionSummary : milestone.transitionSummary !== void 0 && isWorkflowHookRef(milestone.transitionSummary) ? resolveWorkflowHook(milestone.transitionSummary, "transitionSummary") : void 0;
4523
+ const buildTransitionSummary = milestone.transitionSummary === void 0 ? void 0 : transitionHook ?? ((state, nextPhaseName) => interpolateWorkflowTemplate(milestone.transitionSummary, state).replace(
4524
+ /\{\{nextPhase\}\}/g,
4525
+ nextPhaseName
4526
+ ));
4527
+ const recoveryHook = typeof milestone.recovery === "function" ? milestone.recovery : milestone.recovery !== void 0 && isWorkflowHookRef(milestone.recovery) ? resolveWorkflowHook(milestone.recovery, "recovery") : void 0;
4528
+ const buildRecoveryMessage = milestone.recovery === void 0 ? void 0 : recoveryHook ?? ((state) => {
4529
+ const inline = milestone.recovery;
4530
+ const threshold = inline.afterEmptySessions ?? DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS;
4531
+ if ((state.consecutiveEmptySessions ?? 0) < threshold) return void 0;
4532
+ return interpolateWorkflowTemplate(inline.message, state);
4533
+ });
4534
+ const canAcceptCompletion = milestone.canAcceptCompletion === void 0 ? void 0 : typeof milestone.canAcceptCompletion === "function" ? milestone.canAcceptCompletion : isWorkflowHookRef(milestone.canAcceptCompletion) ? resolveWorkflowHook(milestone.canAcceptCompletion, "acceptCompletion") : () => milestone.canAcceptCompletion;
4535
+ const isComplete = typeof milestone.completionCriteria === "function" ? milestone.completionCriteria : buildIsComplete(milestone.completionCriteria, config.name, milestone.name);
4536
+ return {
4537
+ name: milestone.name,
4538
+ description: milestone.description,
4539
+ buildInstructions,
4540
+ buildToolGuidance,
4541
+ isComplete,
4542
+ ...interceptToolCall ? { interceptToolCall } : {},
4543
+ ...buildTransitionSummary ? { buildTransitionSummary } : {},
4544
+ ...buildRecoveryMessage ? { buildRecoveryMessage } : {},
4545
+ ...milestone.forceEndTurn ? { shouldForceEndTurn: resolveSlotHook(milestone.forceEndTurn, "forceEndTurn") } : {},
4546
+ ...canAcceptCompletion ? { canAcceptCompletion } : {}
4547
+ };
4548
+ }
4549
+ function compileWorkflowConfig(config, deps = {}) {
4550
+ const policyIntercept = config.policy ? buildPolicyIntercept(config.policy, config.name, deps) : void 0;
4551
+ const policyGuidance = buildPolicyGuidance(config.policy);
4552
+ const phases = config.milestones.map(
4553
+ (milestone) => compileMilestone(milestone, config, policyIntercept, policyGuidance)
4554
+ );
4555
+ const classifyVariant4 = resolveSlotHook(config.classifyVariant, "classify");
4556
+ const generateBootstrapContext2 = resolveSlotHook(config.bootstrap, "bootstrap");
4557
+ const buildCandidateBlock2 = resolveSlotHook(config.candidateBlock, "candidateBlock");
4558
+ return {
4559
+ name: config.name,
4560
+ phases,
4561
+ ...config.stallPolicy ? { stallPolicy: config.stallPolicy } : {},
4562
+ ...classifyVariant4 ? { classifyVariant: classifyVariant4 } : {},
4563
+ ...generateBootstrapContext2 ? { generateBootstrapContext: generateBootstrapContext2 } : {},
4564
+ ...buildCandidateBlock2 ? { buildCandidateBlock: buildCandidateBlock2 } : {}
4565
+ };
4566
+ }
4567
+
4313
4568
  // src/workflows/default-workflow.ts
4314
4569
  function isExternalTask(state) {
4315
4570
  return state.workflowVariant === "external";
@@ -4459,6 +4714,45 @@ function summarizeTextBlock(value, maxLines = 4) {
4459
4714
  if (!text) return "";
4460
4715
  return text.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, maxLines).join(" | ").slice(0, 240);
4461
4716
  }
4717
+ function interceptProductWriteTarget(toolName, normalizedPathArg, ctx, guardLabel) {
4718
+ const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4719
+ const normalizedBestCandidatePath = ctx.state.bestCandidatePath ? ctx.normalizePath(ctx.state.bestCandidatePath) : void 0;
4720
+ if (!ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4721
+ const allowedWriteTargets = new Set(
4722
+ [
4723
+ normalizedPlanPath,
4724
+ normalizedBestCandidatePath,
4725
+ ...(ctx.state.recentReadPaths || []).map((readPath) => ctx.normalizePath(readPath)),
4726
+ ...ctx.trace.readPaths.map((readPath) => ctx.normalizePath(readPath))
4727
+ ].filter((value) => Boolean(value))
4728
+ );
4729
+ if (!allowedWriteTargets.has(normalizedPathArg)) {
4730
+ return [
4731
+ `Blocked by marathon ${guardLabel}: ${toolName} is limited to the confirmed target, the plan file, or files already discovered/read for this task.`,
4732
+ `Do not create scratch files like "${normalizedPathArg}".`,
4733
+ normalizedBestCandidatePath ? `Edit "${normalizedBestCandidatePath}" or another previously discovered repo file instead.` : "Read the current target file before writing."
4734
+ ].join(" ");
4735
+ }
4736
+ }
4737
+ if (ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4738
+ const outputRoot = ctx.state.outputRoot ? ctx.state.outputRoot.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "") || void 0 : void 0;
4739
+ if (!outputRoot) {
4740
+ return [
4741
+ `Blocked by marathon ${guardLabel}: creation tasks require outputRoot. Writes outside the plan are not allowed.`,
4742
+ `Plan path: "${normalizedPlanPath}". Create files only under the configured output root.`
4743
+ ].join(" ");
4744
+ }
4745
+ const rootPrefix = outputRoot + "/";
4746
+ const isUnderRoot = normalizedPathArg === outputRoot || normalizedPathArg.startsWith(rootPrefix);
4747
+ if (!isUnderRoot) {
4748
+ return [
4749
+ `Blocked by marathon ${guardLabel}: ${toolName} must target the plan or paths under outputRoot "${outputRoot}/".`,
4750
+ `"${normalizedPathArg}" is outside the allowed output root.`
4751
+ ].join(" ");
4752
+ }
4753
+ }
4754
+ return void 0;
4755
+ }
4462
4756
  var researchPhase = {
4463
4757
  name: "research",
4464
4758
  description: "Inspect the repo and identify the correct target file",
@@ -4543,11 +4837,15 @@ var researchPhase = {
4543
4837
  const normalizedPathArg2 = typeof _args.path === "string" && _args.path.trim() ? ctx.normalizePath(String(_args.path)) : void 0;
4544
4838
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4545
4839
  if (normalizedPathArg2 && normalizedPlanPath && normalizedPathArg2 !== normalizedPlanPath) {
4546
- return [
4547
- `Blocked by marathon research guard: ${toolName} cannot create product files during the research phase.`,
4548
- "Complete research first, then the system will advance you to planning.",
4549
- `You may write the plan to "${normalizedPlanPath}" once research is complete.`
4550
- ].join(" ");
4840
+ const planWritten = ctx.trace.planWritten || Boolean(ctx.state.planWritten);
4841
+ if (!planWritten) {
4842
+ return [
4843
+ `Blocked by marathon research guard: ${toolName} cannot create product files during the research phase.`,
4844
+ "Complete research first, then the system will advance you to planning.",
4845
+ `You may write the plan to "${normalizedPlanPath}" once research is complete.`
4846
+ ].join(" ");
4847
+ }
4848
+ return interceptProductWriteTarget(toolName, normalizedPathArg2, ctx, "research guard");
4551
4849
  }
4552
4850
  }
4553
4851
  return void 0;
@@ -4670,19 +4968,24 @@ var planningPhase = {
4670
4968
  "Research is complete. Write the implementation plan for building this from scratch.",
4671
4969
  `Write the plan markdown to exactly: ${planPath}`,
4672
4970
  "List the files you will create, their locations, purpose, and any dependencies to install.",
4971
+ ...state.outputRoot ? [
4972
+ `All new files must be created under "${state.outputRoot}" \u2014 writes outside that directory are blocked, so plan every file location inside it.`
4973
+ ] : [],
4673
4974
  'Include a "Verification steps" section listing the concrete checks you will run before TASK_COMPLETE.',
4674
- "If the plan already exists, update that same plan file instead of creating a different one."
4975
+ "If the plan already exists, update that same plan file instead of creating a different one.",
4976
+ "Once the plan is written, you may begin creating the planned files in the same turn."
4675
4977
  ].join("\n");
4676
4978
  }
4677
4979
  return [
4678
4980
  "--- Workflow Phase: Planning ---",
4679
4981
  "Research is complete. Your current job is to write the implementation plan before any product-file edits.",
4680
4982
  `Write the plan markdown to exactly: ${planPath}`,
4681
- "Do NOT edit the target product file yet.",
4983
+ "Do NOT edit the target product file before the plan exists.",
4682
4984
  "The plan should summarize UX findings, explain why the current best candidate is the right file, and list concrete execution steps.",
4683
4985
  'The plan must include a "Preserve existing functionality" section that lists current behaviors, linked files, integrations, and constraints that must keep working.',
4684
4986
  'The plan must include a "Verification steps" section listing the concrete checks you will run before TASK_COMPLETE.',
4685
- "If the plan already exists, update that same plan file instead of creating a different one."
4987
+ "If the plan already exists, update that same plan file instead of creating a different one.",
4988
+ "Once the plan is written, you may begin editing the target file in the same turn."
4686
4989
  ].join("\n");
4687
4990
  },
4688
4991
  buildToolGuidance(state) {
@@ -4710,10 +5013,14 @@ var planningPhase = {
4710
5013
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4711
5014
  const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
4712
5015
  if (isWriteLikeTool && normalizedPathArg && normalizedPlanPath && normalizedPathArg !== normalizedPlanPath) {
4713
- return [
4714
- `Blocked by marathon planning guard: ${toolName} must target the exact plan path during planning.`,
4715
- `Write the plan to "${normalizedPlanPath}" before editing any product files.`
4716
- ].join(" ");
5016
+ const planWritten = ctx.trace.planWritten || Boolean(ctx.state.planWritten);
5017
+ if (!planWritten) {
5018
+ return [
5019
+ `Blocked by marathon planning guard: ${toolName} must target the exact plan path during planning.`,
5020
+ `Write the plan to "${normalizedPlanPath}" before editing any product files.`
5021
+ ].join(" ");
5022
+ }
5023
+ return interceptProductWriteTarget(toolName, normalizedPathArg, ctx, "planning guard");
4717
5024
  }
4718
5025
  return void 0;
4719
5026
  },
@@ -4773,6 +5080,9 @@ var executionPhase = {
4773
5080
  },
4774
5081
  buildToolGuidance(state) {
4775
5082
  return [
5083
+ ...state.isCreationTask && state.outputRoot ? [
5084
+ `Creation guard: create new files under "${state.outputRoot}". Writes outside it are blocked \u2014 the plan file is the only exception.`
5085
+ ] : [],
4776
5086
  ...state.bestCandidatePath ? [
4777
5087
  `Execution-phase guard: broad discovery tools (search_repo, glob_files, tree_directory, list_directory) are locked while executing against "${state.bestCandidatePath}".`
4778
5088
  ] : [
@@ -4814,40 +5124,13 @@ var executionPhase = {
4814
5124
  `After that, you may update "${normalizedPlanPath}" with progress.`
4815
5125
  ].join(" ");
4816
5126
  }
4817
- if (!ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4818
- const allowedWriteTargets = new Set(
4819
- [
4820
- normalizedPlanPath,
4821
- normalizedBestCandidatePath,
4822
- ...(ctx.state.recentReadPaths || []).map((readPath) => ctx.normalizePath(readPath)),
4823
- ...ctx.trace.readPaths.map((readPath) => ctx.normalizePath(readPath))
4824
- ].filter((value) => Boolean(value))
4825
- );
4826
- if (!allowedWriteTargets.has(normalizedPathArg)) {
4827
- return [
4828
- `Blocked by marathon execution guard: ${toolName} is limited to the confirmed target, the plan file, or files already discovered/read for this task.`,
4829
- `Do not create scratch files like "${normalizedPathArg}".`,
4830
- normalizedBestCandidatePath ? `Edit "${normalizedBestCandidatePath}" or another previously discovered repo file instead.` : "Read the current target file before writing."
4831
- ].join(" ");
4832
- }
4833
- }
4834
- if (ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4835
- const outputRoot = ctx.state.outputRoot ? ctx.state.outputRoot.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "") || void 0 : void 0;
4836
- if (!outputRoot) {
4837
- return [
4838
- `Blocked by marathon execution guard: creation tasks require outputRoot. Writes outside the plan are not allowed.`,
4839
- `Plan path: "${normalizedPlanPath}". Create files only under the configured output root.`
4840
- ].join(" ");
4841
- }
4842
- const rootPrefix = outputRoot + "/";
4843
- const isUnderRoot = normalizedPathArg === outputRoot || normalizedPathArg.startsWith(rootPrefix);
4844
- if (!isUnderRoot) {
4845
- return [
4846
- `Blocked by marathon execution guard: ${toolName} must target the plan or paths under outputRoot "${outputRoot}/".`,
4847
- `"${normalizedPathArg}" is outside the allowed output root.`
4848
- ].join(" ");
4849
- }
4850
- }
5127
+ const writeTargetBlock = interceptProductWriteTarget(
5128
+ toolName,
5129
+ normalizedPathArg,
5130
+ ctx,
5131
+ "execution guard"
5132
+ );
5133
+ if (writeTargetBlock) return writeTargetBlock;
4851
5134
  }
4852
5135
  return void 0;
4853
5136
  },
@@ -5080,13 +5363,163 @@ function buildCandidateBlock(state) {
5080
5363
  ...state.bestCandidateReason ? [`Why: ${state.bestCandidateReason}`] : []
5081
5364
  ].join("\n");
5082
5365
  }
5083
- var defaultWorkflow = {
5366
+ var builtinHooksRegistered = false;
5367
+ function ensureDefaultWorkflowHooks() {
5368
+ if (builtinHooksRegistered) return;
5369
+ builtinHooksRegistered = true;
5370
+ registerBuiltinWorkflowHook("builtin:classify-task-variant", {
5371
+ kind: "classify",
5372
+ fn: classifyVariant
5373
+ });
5374
+ registerBuiltinWorkflowHook("builtin:repo-bootstrap-discovery", {
5375
+ kind: "bootstrap",
5376
+ fn: generateBootstrapContext
5377
+ });
5378
+ registerBuiltinWorkflowHook("builtin:best-candidate-block", {
5379
+ kind: "candidateBlock",
5380
+ fn: buildCandidateBlock
5381
+ });
5382
+ registerBuiltinWorkflowHook("builtin:research-instructions", {
5383
+ kind: "instructions",
5384
+ fn: researchPhase.buildInstructions
5385
+ });
5386
+ registerBuiltinWorkflowHook("builtin:research-tool-guidance", {
5387
+ kind: "toolGuidance",
5388
+ fn: researchPhase.buildToolGuidance
5389
+ });
5390
+ registerBuiltinWorkflowHook("builtin:research-complete", {
5391
+ kind: "completion",
5392
+ fn: researchPhase.isComplete
5393
+ });
5394
+ registerBuiltinWorkflowHook("builtin:research-transition-summary", {
5395
+ kind: "transitionSummary",
5396
+ fn: researchPhase.buildTransitionSummary
5397
+ });
5398
+ registerBuiltinWorkflowHook("builtin:research-guard", {
5399
+ kind: "intercept",
5400
+ fn: researchPhase.interceptToolCall
5401
+ });
5402
+ registerBuiltinWorkflowHook("builtin:research-recovery", {
5403
+ kind: "recovery",
5404
+ fn: researchPhase.buildRecoveryMessage
5405
+ });
5406
+ registerBuiltinWorkflowHook("builtin:research-force-end-turn", {
5407
+ kind: "forceEndTurn",
5408
+ fn: researchPhase.shouldForceEndTurn
5409
+ });
5410
+ registerBuiltinWorkflowHook("builtin:research-accept-completion", {
5411
+ kind: "acceptCompletion",
5412
+ fn: researchPhase.canAcceptCompletion
5413
+ });
5414
+ registerBuiltinWorkflowHook("builtin:planning-instructions", {
5415
+ kind: "instructions",
5416
+ fn: planningPhase.buildInstructions
5417
+ });
5418
+ registerBuiltinWorkflowHook("builtin:planning-tool-guidance", {
5419
+ kind: "toolGuidance",
5420
+ fn: planningPhase.buildToolGuidance
5421
+ });
5422
+ registerBuiltinWorkflowHook("builtin:planning-complete", {
5423
+ kind: "completion",
5424
+ fn: planningPhase.isComplete
5425
+ });
5426
+ registerBuiltinWorkflowHook("builtin:planning-transition-summary", {
5427
+ kind: "transitionSummary",
5428
+ fn: planningPhase.buildTransitionSummary
5429
+ });
5430
+ registerBuiltinWorkflowHook("builtin:planning-guard", {
5431
+ kind: "intercept",
5432
+ fn: planningPhase.interceptToolCall
5433
+ });
5434
+ registerBuiltinWorkflowHook("builtin:planning-recovery", {
5435
+ kind: "recovery",
5436
+ fn: planningPhase.buildRecoveryMessage
5437
+ });
5438
+ registerBuiltinWorkflowHook("builtin:planning-force-end-turn", {
5439
+ kind: "forceEndTurn",
5440
+ fn: planningPhase.shouldForceEndTurn
5441
+ });
5442
+ registerBuiltinWorkflowHook("builtin:execution-instructions", {
5443
+ kind: "instructions",
5444
+ fn: executionPhase.buildInstructions
5445
+ });
5446
+ registerBuiltinWorkflowHook("builtin:execution-tool-guidance", {
5447
+ kind: "toolGuidance",
5448
+ fn: executionPhase.buildToolGuidance
5449
+ });
5450
+ registerBuiltinWorkflowHook("builtin:execution-guard", {
5451
+ kind: "intercept",
5452
+ fn: executionPhase.interceptToolCall
5453
+ });
5454
+ registerBuiltinWorkflowHook("builtin:execution-recovery", {
5455
+ kind: "recovery",
5456
+ fn: executionPhase.buildRecoveryMessage
5457
+ });
5458
+ registerBuiltinWorkflowHook("builtin:execution-force-end-turn", {
5459
+ kind: "forceEndTurn",
5460
+ fn: executionPhase.shouldForceEndTurn
5461
+ });
5462
+ registerBuiltinWorkflowHook("builtin:execution-accept-completion", {
5463
+ kind: "acceptCompletion",
5464
+ fn: executionPhase.canAcceptCompletion
5465
+ });
5466
+ }
5467
+ var defaultWorkflowConfig = {
5084
5468
  name: "default",
5085
- phases: [researchPhase, planningPhase, executionPhase],
5086
- classifyVariant,
5087
- generateBootstrapContext,
5088
- buildCandidateBlock
5469
+ // Empty-session escalation. The counter only counts tool actions, so
5470
+ // narration-only sessions ("I'll create the files now" with no tool calls)
5471
+ // escalate here even though the phase recovery conditions keyed on
5472
+ // hadTextOutput skip them: nudge after the first actionless session, signal
5473
+ // model escalation after the second (a no-op unless the caller configured a
5474
+ // fallback model), and stop as 'stalled' after the third — the same total
5475
+ // session budget as before stallPolicy existed.
5476
+ stallPolicy: { nudgeAfter: 1, escalateModelAfter: 2, stopAfter: 3 },
5477
+ classifyVariant: "builtin:classify-task-variant",
5478
+ bootstrap: "builtin:repo-bootstrap-discovery",
5479
+ candidateBlock: "builtin:best-candidate-block",
5480
+ milestones: [
5481
+ {
5482
+ name: "research",
5483
+ description: "Inspect the repo and identify the correct target file",
5484
+ instructions: "builtin:research-instructions",
5485
+ toolGuidance: "builtin:research-tool-guidance",
5486
+ completionCriteria: { type: "builtin:research-complete" },
5487
+ intercept: "builtin:research-guard",
5488
+ transitionSummary: "builtin:research-transition-summary",
5489
+ recovery: "builtin:research-recovery",
5490
+ forceEndTurn: "builtin:research-force-end-turn",
5491
+ canAcceptCompletion: "builtin:research-accept-completion"
5492
+ },
5493
+ {
5494
+ name: "planning",
5495
+ description: "Write the implementation plan before editing product files",
5496
+ instructions: "builtin:planning-instructions",
5497
+ toolGuidance: "builtin:planning-tool-guidance",
5498
+ completionCriteria: { type: "builtin:planning-complete" },
5499
+ intercept: "builtin:planning-guard",
5500
+ transitionSummary: "builtin:planning-transition-summary",
5501
+ recovery: "builtin:planning-recovery",
5502
+ forceEndTurn: "builtin:planning-force-end-turn"
5503
+ // canAcceptCompletion intentionally absent: the hand-written planning
5504
+ // phase never defined it, and the SDK accepts completion when the slot
5505
+ // is undefined. Keep parity.
5506
+ },
5507
+ {
5508
+ name: "execution",
5509
+ description: "Execute the plan by editing target files",
5510
+ instructions: "builtin:execution-instructions",
5511
+ toolGuidance: "builtin:execution-tool-guidance",
5512
+ // Execution never auto-advances; completion is agent-driven via TASK_COMPLETE
5513
+ completionCriteria: { type: "never" },
5514
+ intercept: "builtin:execution-guard",
5515
+ recovery: "builtin:execution-recovery",
5516
+ forceEndTurn: "builtin:execution-force-end-turn",
5517
+ canAcceptCompletion: "builtin:execution-accept-completion"
5518
+ }
5519
+ ]
5089
5520
  };
5521
+ ensureDefaultWorkflowHooks();
5522
+ var defaultWorkflow = compileWorkflowConfig(defaultWorkflowConfig);
5090
5523
 
5091
5524
  // src/workflows/deploy-workflow.ts
5092
5525
  var scaffoldPhase = {
@@ -5498,6 +5931,34 @@ var gameWorkflow = {
5498
5931
  }
5499
5932
  };
5500
5933
 
5934
+ // src/workflows/stall-policy.ts
5935
+ var DEFAULT_STALL_STOP_AFTER = 3;
5936
+ function isPositiveInteger(value) {
5937
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
5938
+ }
5939
+ function resolveStallStopAfter(policy) {
5940
+ return isPositiveInteger(policy?.stopAfter) ? policy.stopAfter : DEFAULT_STALL_STOP_AFTER;
5941
+ }
5942
+ function shouldRequestModelEscalation(policy, consecutiveEmptySessions) {
5943
+ const threshold = policy?.escalateModelAfter;
5944
+ if (!isPositiveInteger(threshold)) return false;
5945
+ return consecutiveEmptySessions === threshold;
5946
+ }
5947
+ function shouldInjectEmptySessionNudge(policy, consecutiveEmptySessions) {
5948
+ const threshold = policy?.nudgeAfter;
5949
+ if (!isPositiveInteger(threshold)) return false;
5950
+ return consecutiveEmptySessions >= threshold;
5951
+ }
5952
+ function buildEmptySessionNudge(consecutiveEmptySessions) {
5953
+ const sessionPhrase = consecutiveEmptySessions === 1 ? "Your previous session ended" : `Your previous ${consecutiveEmptySessions} sessions ended`;
5954
+ return [
5955
+ "Recovery instruction:",
5956
+ `${sessionPhrase} without a single tool call. Describing what you plan to do does nothing \u2014 only tool calls make progress.`,
5957
+ "Your next response MUST include at least one tool call (for example write_file, edit_file, read_file, or run_check) that advances the task.",
5958
+ "If a previous tool call was blocked, re-read the block message and satisfy its requirement instead of ending the turn."
5959
+ ].join("\n");
5960
+ }
5961
+
5501
5962
  // src/endpoints.ts
5502
5963
  var FlowsEndpoint = class {
5503
5964
  constructor(client) {
@@ -8044,8 +8505,11 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8044
8505
  }
8045
8506
  buildStuckTurnRecoveryMessage(state, workflow) {
8046
8507
  const currentPhase = workflow.phases.find((p) => p.name === state.workflowPhase);
8047
- if (currentPhase?.buildRecoveryMessage) {
8048
- return currentPhase.buildRecoveryMessage(state);
8508
+ const phaseMessage = currentPhase?.buildRecoveryMessage?.(state);
8509
+ if (phaseMessage) return phaseMessage;
8510
+ const emptySessions = state.consecutiveEmptySessions || 0;
8511
+ if (shouldInjectEmptySessionNudge(workflow.stallPolicy, emptySessions)) {
8512
+ return buildEmptySessionNudge(emptySessions);
8049
8513
  }
8050
8514
  return void 0;
8051
8515
  }
@@ -8171,6 +8635,10 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8171
8635
  state.originalMessage = options.message;
8172
8636
  }
8173
8637
  const queuedSteeringMessages = options.getQueuedUserMessages?.() ?? [];
8638
+ if (queuedSteeringMessages.length > 0) {
8639
+ state.consecutiveEmptySessions = 0;
8640
+ state.stallEscalationRequested = void 0;
8641
+ }
8174
8642
  const preparedSession = await this.prepareSessionContext(
8175
8643
  options.message,
8176
8644
  state,
@@ -8411,11 +8879,15 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8411
8879
  } else {
8412
8880
  state.lastCompletionRejectionReason = void 0;
8413
8881
  }
8414
- const sessionHadActions = sessionTrace.wroteFiles || sessionTrace.readFiles || sessionTrace.discoveryPerformed || sessionTrace.verificationAttempted;
8882
+ const sessionHadActions = sessionTrace.wroteFiles || sessionTrace.readFiles || sessionTrace.discoveryPerformed || sessionTrace.verificationAttempted || options.hasQueuedUserMessages?.() === true;
8415
8883
  if (sessionHadActions) {
8416
8884
  state.consecutiveEmptySessions = 0;
8885
+ state.stallEscalationRequested = void 0;
8417
8886
  } else {
8418
8887
  state.consecutiveEmptySessions = (state.consecutiveEmptySessions || 0) + 1;
8888
+ if (shouldRequestModelEscalation(workflow.stallPolicy, state.consecutiveEmptySessions)) {
8889
+ state.stallEscalationRequested = true;
8890
+ }
8419
8891
  }
8420
8892
  if (sessionResult.stopReason === "complete" && !detectedTaskCompletion) {
8421
8893
  const currentPhase = workflow.phases.find((p) => p.name === state.workflowPhase);
@@ -8444,7 +8916,7 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8444
8916
  state.status = "budget_exceeded";
8445
8917
  } else if (acceptedTaskCompletion) {
8446
8918
  state.status = "complete";
8447
- } else if ((state.consecutiveEmptySessions || 0) >= 3) {
8919
+ } else if ((state.consecutiveEmptySessions || 0) >= resolveStallStopAfter(workflow.stallPolicy)) {
8448
8920
  state.status = "stalled";
8449
8921
  } else if (maxCost && state.totalCost >= maxCost) {
8450
8922
  state.status = "budget_exceeded";
@@ -10993,6 +11465,8 @@ export {
10993
11465
  ClientTokensEndpoint,
10994
11466
  ContextTemplatesEndpoint,
10995
11467
  ConversationsEndpoint,
11468
+ DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS,
11469
+ DEFAULT_STALL_STOP_AFTER,
10996
11470
  DispatchEndpoint,
10997
11471
  EvalBuilder,
10998
11472
  EvalEndpoint,
@@ -11030,25 +11504,34 @@ export {
11030
11504
  UsersEndpoint,
11031
11505
  applyGeneratedRuntimeToolProposalToDispatchRequest,
11032
11506
  attachRuntimeToolsToDispatchRequest,
11507
+ buildEmptySessionNudge,
11033
11508
  buildGeneratedRuntimeToolGateOutput,
11034
11509
  buildLedgerOffloadReference,
11510
+ buildPolicyGuidance,
11035
11511
  buildSendViewOffloadMarker,
11512
+ compileWorkflowConfig,
11036
11513
  computeAgentContentHash,
11037
11514
  computeFlowContentHash,
11038
11515
  createClient,
11039
11516
  createExternalTool,
11040
11517
  defaultWorkflow,
11518
+ defaultWorkflowConfig,
11041
11519
  defineAgent,
11042
11520
  defineFlow,
11521
+ definePlaybook,
11043
11522
  deployWorkflow,
11523
+ ensureDefaultWorkflowHooks,
11044
11524
  evaluateGeneratedRuntimeToolProposal,
11045
11525
  extractDeclaredToolResultChars,
11046
11526
  gameWorkflow,
11047
11527
  getDefaultPlanPath,
11048
11528
  getLikelySupportingCandidatePaths,
11529
+ interpolateWorkflowTemplate,
11049
11530
  isDiscoveryToolName,
11050
11531
  isMarathonArtifactPath,
11051
11532
  isPreservationSensitiveTask,
11533
+ isWorkflowHookRef,
11534
+ listWorkflowHooks,
11052
11535
  normalizeAgentDefinition,
11053
11536
  normalizeCandidatePath,
11054
11537
  parseFinalBuffer,
@@ -11056,6 +11539,12 @@ export {
11056
11539
  parseOffloadedOutputId,
11057
11540
  parseSSEChunk,
11058
11541
  processStream,
11542
+ registerWorkflowHook,
11543
+ resolveStallStopAfter,
11544
+ resolveWorkflowHook,
11059
11545
  sanitizeTaskSlug,
11060
- streamEvents
11546
+ shouldInjectEmptySessionNudge,
11547
+ shouldRequestModelEscalation,
11548
+ streamEvents,
11549
+ unregisterWorkflowHook
11061
11550
  };