@rosthq/cli 0.7.24 → 0.7.26

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.js CHANGED
@@ -37718,6 +37718,19 @@ var dryRunToolActionSchema = external_exports.object({
37718
37718
  // The underlying manifest guard decision that produced `status`.
37719
37719
  guard: dryRunGuardResultSchema
37720
37720
  }).strict();
37721
+ var dryRunLaneSchema = external_exports.enum(["cloud", "mcp_session", "runner"]);
37722
+ var dryRunToolExecutabilitySchema = external_exports.enum([
37723
+ "executable",
37724
+ "unimplemented_in_lane",
37725
+ "held_for_steward"
37726
+ ]);
37727
+ var dryRunGrantedToolSchema = external_exports.object({
37728
+ // The granted tool (manifest id).
37729
+ tool_name: external_exports.string().min(1),
37730
+ // Its lane-aware executability, derived from the agent's lane + the runtime
37731
+ // handler registry (never an id string pattern).
37732
+ executability: dryRunToolExecutabilitySchema
37733
+ }).strict();
37721
37734
  var dryRunTranscriptSchema = external_exports.object({
37722
37735
  // Whether the rehearsal passed (the must-escalate clause was escalated and no
37723
37736
  // external side effect ran) or failed (it acted without escalating, or the run
@@ -37736,6 +37749,14 @@ var dryRunTranscriptSchema = external_exports.object({
37736
37749
  // handlers. Optional so the field is additive — a transcript without it (the
37737
37750
  // static demo replay, an older record) still validates.
37738
37751
  tool_preview: external_exports.array(dryRunToolActionSchema).optional(),
37752
+ // DER-1294: the runtime lane this rehearsal reflects. Optional/additive —
37753
+ // transcripts recorded before DER-1294 (and the static public-demo replay)
37754
+ // omit it, so they still validate.
37755
+ lane: dryRunLaneSchema.optional(),
37756
+ // DER-1294: every GRANTED tool (manifest allow|escalate) with its lane-aware
37757
+ // executability, so an operator sees which grants actually run vs. silently
37758
+ // no-op in this lane. Optional/additive so older transcripts still validate.
37759
+ granted_tools: external_exports.array(dryRunGrantedToolSchema).optional(),
37739
37760
  // The underlying run id (present once the run is recorded).
37740
37761
  run_id: external_exports.string().min(1).optional()
37741
37762
  }).strict();
@@ -42457,6 +42478,10 @@ var softwareBuildRequestSummarySchema = external_exports.object({
42457
42478
  current_phase: softwarePhaseSchema,
42458
42479
  inferred_risk: softwareRiskLevelSchema,
42459
42480
  automation_mode: softwareAutomationModeSchema,
42481
+ max_iterations: external_exports.number().int().nullable(),
42482
+ max_wall_clock_seconds: external_exports.number().int().nullable(),
42483
+ cost_budget_usd: external_exports.string().nullable(),
42484
+ iterations_used: external_exports.number().int(),
42460
42485
  created_at: external_exports.string()
42461
42486
  });
42462
42487
  var softwarePhaseRunSummarySchema = external_exports.object({
@@ -42510,6 +42535,21 @@ var softwareTaskSummarySchema = external_exports.object({
42510
42535
  completed_at: external_exports.string().nullable(),
42511
42536
  created_at: external_exports.string()
42512
42537
  });
42538
+ var softwarePlanVersionSummarySchema = external_exports.object({
42539
+ id: uuid10,
42540
+ build_request_id: uuid10,
42541
+ version_no: external_exports.number().int(),
42542
+ status: softwarePlanVersionStatusSchema,
42543
+ summary: external_exports.string().nullable(),
42544
+ confidence_score: external_exports.number().int().nullable(),
42545
+ clarifying_questions: external_exports.array(external_exports.string()).default([]),
42546
+ clarification_answers: external_exports.array(external_exports.object({
42547
+ question: external_exports.string(),
42548
+ answer: external_exports.string()
42549
+ })).default([]),
42550
+ created_by_seat_id: uuid10.nullable(),
42551
+ created_at: external_exports.string()
42552
+ });
42513
42553
  var softwarePlanConformanceFindingSummarySchema = external_exports.object({
42514
42554
  id: uuid10,
42515
42555
  build_request_id: uuid10,
@@ -42607,7 +42647,10 @@ var softwareRequestShowOutputSchema = external_exports.object({
42607
42647
  request: softwareBuildRequestSummarySchema,
42608
42648
  current_phase: softwarePhaseSchema,
42609
42649
  phase_runs: external_exports.array(softwarePhaseRunSummarySchema),
42610
- gates: external_exports.array(softwareGateSummarySchema)
42650
+ gates: external_exports.array(softwareGateSummarySchema),
42651
+ plan_versions: external_exports.array(softwarePlanVersionSummarySchema),
42652
+ changesets: external_exports.array(softwareChangesetSummarySchema),
42653
+ tasks: external_exports.array(softwareTaskSummarySchema)
42611
42654
  });
42612
42655
  var softwarePhaseAdvanceInputSchema = external_exports.object({
42613
42656
  build_request_id: uuid10,
@@ -44367,7 +44410,7 @@ No orphan agents. No raw secrets in prompts, logs, or tool arguments. No durable
44367
44410
  order: 41,
44368
44411
  title: "Add agents to your Responsibility Graph",
44369
44412
  summary: "The visual journey for adding an agent seat: where to start, choosing a mode, placing the seat, naming a Steward, setup, the safety gates, and go-live.",
44370
- version: "2026-06-21.3",
44413
+ version: "2026-07-04.1",
44371
44414
  public: true,
44372
44415
  audiences: ["human", "in_app_agent"],
44373
44416
  stages: ["staffing"],
@@ -44416,8 +44459,8 @@ The wizard builds a teammate, not a prompt:
44416
44459
 
44417
44460
  1. **Responsibilities and authority** \u2014 what the seat owns, what it may do alone, what needs approval, and what must escalate. Every clause is editable before sign-off.
44418
44461
  2. **Signals** \u2014 the measurables the seat is judged on.
44419
- 3. **Skills** \u2014 pin published, immutable Skill versions that the agent should carry. Skills are procedures, not authority: selecting one creates a proposed assignment and surfaces required/optional tool dependencies before sign-off.
44420
- 4. **Tools** \u2014 connect or decline each proposed tool. Declining a tool updates the permission manifest and the dry-run task so the agent stays safe. Credentials go in through write-only credential ingress and are stored as vault references, never shown again. Missing required Skill tools block sign-off, dry-run, and go-live until resolved; missing optional Skill tools warn without blocking.
44462
+ 3. **Skills** \u2014 pin published, immutable Skill versions that the agent should carry. Skills are procedures, not authority: selecting one creates a proposed assignment and surfaces required/optional tool dependencies before access approval. The builder shows the provider and Access level each Skill proposes so the human sees the tool bundle before it enters the draft manifest.
44463
+ 4. **Access** \u2014 connect or decline each proposed tool. Declining a tool updates the permission manifest and the dry-run task so the agent stays safe. Secrets go in through write-only credential ingress and are stored as vault references, never shown again. Missing required Skill tools block access approval, dry-run, and go-live until resolved; missing optional Skill tools warn without blocking.
44421
44464
  5. **Schedule and lane** \u2014 when the agent runs and on which lane. These stay advanced unless the mode needs them.
44422
44465
 
44423
44466
  You can save and resume a draft setup at any point. Returning to the flow restores the parent, Steward, mode, answers, Skill selections, tool decisions, and dry-run result.
@@ -44427,7 +44470,7 @@ You can save and resume a draft setup at any point. Returning to the flow restor
44427
44470
  Every mode ends at the same conservative gates, shown at each step:
44428
44471
 
44429
44472
  1. **Draft Charter** \u2014 a draft, not an active job, until a human approves it.
44430
- 2. **Signed manifest** \u2014 a human signs the permission manifest before any tool is live.
44473
+ 2. **Approve access** \u2014 a human approves the permission manifest before any tool is live. The command remains \`charter.sign_manifest\`; the UI names the human decision, not the storage primitive.
44431
44474
  3. **Sandbox dry run** \u2014 the agent rehearses a representative task and is expected to escalate where the Charter says it must. A failed dry run keeps the draft and shows the reason; you edit and rerun.
44432
44475
  4. **Human go-live** \u2014 a person promotes the agent live. An agent never approves its own setup, signs its own manifest, or goes itself live.
44433
44476
 
@@ -44443,14 +44486,14 @@ Reopening the builder for a seat whose agent is already live shows its live stat
44443
44486
 
44444
44487
  In read-only or demo mode the **Add agent** affordance never starts a write. The public demo instead replays the add-an-agent journey end to end \u2014 describe the role, watch the draft Charter assemble, see the four safety gates light, and watch a sandbox dry run reach the must-escalate boundary and stop \u2014 then routes go-live to sign-up, because going live is a human decision.
44445
44488
 
44446
- For CLI and MCP setup, read agent-skill-setup-guide before adding or assigning Skills. List existing company Skills and the {{brand}} catalog first, read the full Skill file before proposing it, and run the dependency check before sign-off. A missing required Skill tool is fixed by updating the Charter permission manifest through tool configuration; it is never fixed by treating the Skill as authorization.`
44489
+ For CLI and MCP setup, read agent-skill-setup-guide before adding or assigning Skills. List existing company Skills and the {{brand}} catalog first, read the full Skill file before proposing it, and run the dependency check before access approval. A missing required Skill tool is fixed by updating the Charter permission manifest through tool configuration; it is never fixed by treating the Skill as authorization.`
44447
44490
  },
44448
44491
  {
44449
44492
  slug: "custom-agents-guide",
44450
44493
  order: 42,
44451
44494
  title: "Design a custom agent",
44452
44495
  summary: "How to build a custom agent from operational questions through the Charter Builder, tools, dry run, and go-live without writing prompts.",
44453
- version: "2026-06-21.1",
44496
+ version: "2026-07-04.1",
44454
44497
  public: true,
44455
44498
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44456
44499
  stages: ["staffing", "charter_design"],
@@ -44517,7 +44560,7 @@ The same path is command-backed:
44517
44560
 
44518
44561
  ## Dry run and go-live
44519
44562
 
44520
- The dry run is a real sandbox rehearsal, not a stamp. It executes a mock-provider run derived from the Charter \u2014 the agent works against sandbox data only and is expected to escalate where the Charter's must-escalate clause requires it. The same rehearsal works on all three lanes: cloud, external MCP, and local Runner. External MCP dry runs require an active seat-scoped MCP token; Runner dry runs require a paired Runner. Missing substrate returns a typed precondition error, not a generic failure. The recorded run keeps the agent's actual lane, so the evidence you review matches the lane you selected. The result is earned: a run that escalates the must-escalate boundary passes; a run that acts on that boundary instead of escalating fails. A failed dry run keeps the draft and shows the reason so you can edit and rerun. The rehearsal returns a transcript \u2014 the steps the agent took and the escalation it raised \u2014 shown step by step in the builder and printed by the CLI, so you can see the governance model working before anything goes live. When the dry run passes, a human promotes the agent live. The dry run rehearses the specific model tier you chose, so once it passes the model is locked \u2014 changing the model requires re-running the dry run on the new model before go-live.
44563
+ The dry run is a real sandbox rehearsal, not a stamp. It executes a mock-provider run derived from the Charter \u2014 the agent works against sandbox data only and is expected to escalate where the Charter's must-escalate clause requires it. The same rehearsal works on all three lanes: cloud, external MCP, and local Runner. External MCP dry runs require an active seat-scoped MCP token; Runner dry runs require a paired Runner. Missing substrate returns a typed precondition error, not a generic failure. The recorded run keeps the agent's actual lane, so the evidence you review matches the lane you selected. The result is earned: a run that escalates the must-escalate boundary passes; a run that acts on that boundary instead of escalating fails. A failed dry run keeps the draft and shows the reason so you can edit and rerun. The rehearsal returns a transcript \u2014 the steps the agent took and the escalation it raised \u2014 shown step by step in the builder and printed by the CLI, so you can see the governance model working before anything goes live. The transcript is also lane-honest: each granted tool is marked executable, held for the steward, or granted but unimplemented in this lane, with a plain-count verdict such as "2 of 6 granted tools don't execute in the cloud lane yet" \u2014 so a grant that would silently no-op in the cloud runtime is visible before go-live instead of passing green. Runner and MCP-session agents run those tools through their own local skills, so their grants are not flagged as unimplemented. When the dry run passes, a human promotes the agent live. The dry run rehearses the specific model tier you chose, so once it passes the model is locked \u2014 changing the model requires re-running the dry run on the new model before go-live.
44521
44564
 
44522
44565
  ## When to stop for confirmation
44523
44566
 
@@ -44860,7 +44903,7 @@ External connectors are being rolled out provider by provider, conservatively (r
44860
44903
  order: 48,
44861
44904
  title: "CLI and MCP installation guide",
44862
44905
  summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
44863
- version: "2026-07-04.16",
44906
+ version: "2026-07-04.17",
44864
44907
  public: true,
44865
44908
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44866
44909
  stages: ["company_setup", "staffing"],
@@ -45603,7 +45646,7 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
45603
45646
  | \`rost_list_forge_github_installations\` | \`software_factory.github.installation.list\` | List connected GitHub App installations and each repository's bind state (connected or active) with the Forge project it maps to. Read-only. | Tenant | Call with \`{}\`; a \`connected\` repository is verified but not yet bound to a project. |
45604
45647
  | \`rost_list_forge_runner_capacity\` | \`software_factory.capacity.list\` | List Forge runner capacity observations (advisory scheduling input only, never an authority input). Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"runner_id":"<runner-id>"}\`. |
45605
45648
 
45606
- The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, runner capacity, configuration status, and Developer Team install. The install button deliberately stages a pending confirmation before durable seats, Skills, authority grants, or live agent activation are applied; the install page states plainly that approving activates the 6 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
45649
+ The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, runner capacity, configuration status, and Developer Team install. A request detail page shows phase history, open gates, plan progress, loop/time/cost ceilings, and latest plan-review questions from \`software_factory.request.show\`; answering a clarification, approving a plan, or rejecting it calls the same \`software_factory.request.answer_clarification\`, \`software_factory.plan.approve\`, and \`software_factory.plan.reject\` commands exposed over CLI/MCP. The stop control is visible only as a non-executing placeholder until pause/cancel commands are wired, so the UI does not imply a capability that is not live. The install button deliberately stages a pending confirmation before durable seats, Skills, authority grants, or live agent activation are applied; the install page states plainly that approving activates the 6 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
45607
45650
 
45608
45651
  #### Forge GitHub App access
45609
45652
 
@@ -46089,7 +46132,7 @@ Retrieve the narrowest relevant guide before making a setup recommendation. Pref
46089
46132
  order: 60,
46090
46133
  title: "Cascade guide",
46091
46134
  summary: "How to connect company goals to seat-level work without turning {{brand}} into a project-management tool.",
46092
- version: "2026-07-04.1",
46135
+ version: "2026-07-04.2",
46093
46136
  public: true,
46094
46137
  audiences: ["human", "cli", "mcp", "in_app_agent"],
46095
46138
  stages: ["operating_rhythm"],
@@ -46157,7 +46200,7 @@ Cascade warnings are advisory, not blockers. They point to setup that should be
46157
46200
  - Agent goal missing human accountability parent: an agent-owned goal has no human or hybrid goal in its ancestry.
46158
46201
  - Orphaned goal branch: the owner seat or parent chain no longer reaches an active Compass objective.
46159
46202
 
46160
- On \`/cascade\`, the screen renders the full goal depth, not just one or two levels. Off-track descendant branches stay open so the operator can see why a parent rolled up to risk.
46203
+ \`/cascade\` leads with one computed line \u2014 how many goals are off pace this cycle, or an all-clear when none are. Off-pace and off-track goals stay fully visible with their owner seat and pace context; goals confirmed on pace collapse into a per-branch count you can expand. Creating or editing a goal opens the same drawer from a single **New goal** action or a goal's own edit control \u2014 there is no separate persistent form.
46161
46204
 
46162
46205
  ## Goal progress and at-risk goals
46163
46206
 
@@ -46304,7 +46347,7 @@ Agents may record readings when the Charter allows it. Agent-reported readings s
46304
46347
  order: 62,
46305
46348
  title: "Friction guide",
46306
46349
  summary: "How to capture issues with evidence, rank them, and resolve them without losing ownership.",
46307
- version: "2026-07-01.2",
46350
+ version: "2026-07-04.1",
46308
46351
  public: true,
46309
46352
  audiences: ["human", "cli", "mcp", "in_app_agent"],
46310
46353
  stages: ["operating_rhythm"],
@@ -46356,6 +46399,8 @@ Agents should file Friction when a measurable breaks, a tool fails, a repeated e
46356
46399
 
46357
46400
  In the app, operational evidence is summarized for humans instead of shown as the primary interface. Incident fields, proposed tool calls, schedules, payload labels, and source links render as readable fields with raw details tucked behind supporting context when useful. Secret-shaped values stay redacted.
46358
46401
 
46402
+ The Friction page opens with one computed line \u2014 how many issues are open, the oldest one's age, and how many are escalated \u2014 followed by the open issues themselves as incident-ticket rows (filer, one-line cause, an escalation chip naming the steward when severity is critical). Resolved issues collapse into a single count that expands into a quiet history list. Filing a new issue and the three-step resolution (root cause, owner task, done-by) both live behind one "File friction" action, which opens a drawer; resolving launches that same drawer already seeded from the row you clicked "Resolve" on.
46403
+
46359
46404
  ## Operate Friction, tasks, and escalations from CLI or MCP
46360
46405
 
46361
46406
  Friction, tasks, and escalations are the issue-to-action loop. A seat files, a task carries the work, and an escalation routes a decision a seat cannot make alone.
@@ -46617,7 +46662,7 @@ Do not claim that a live charge happened unless Stripe test/live evidence proves
46617
46662
  order: 72,
46618
46663
  title: "Settings guide",
46619
46664
  summary: "How to use Settings as the control plane for company access, channels, providers, tokens, and operating defaults.",
46620
- version: "2026-07-03.2",
46665
+ version: "2026-07-04.1",
46621
46666
  public: true,
46622
46667
  audiences: ["human", "cli", "mcp", "in_app_agent"],
46623
46668
  stages: ["company_setup", "staffing"],
@@ -46640,7 +46685,9 @@ Start with members and invites, then provider and channel connections, then MCP
46640
46685
 
46641
46686
  ## Navigating Settings
46642
46687
 
46643
- Settings is a left-rail master\u2013detail layout: a grouped rail (General, Team & Access, Billing, AI & Agents, Connections) selects one section at a time, shown in the detail pane. The active section is a URL search param, so \`/settings?section=<id>\` is linkable and survives a refresh. Legacy hash deep-links still resolve \u2014 \`/settings#integrations\`, \`#members\`, \`#runners\`, \`#guardrails\`, \`#vault\` and the rest redirect to the matching \`?section=\` while preserving any status params. Documented shortcut links resolve to the right section instead of a dead end: \`/settings/integrations\` and \`/readiness\` open the Integrations section (connector readiness lives inside it, not on a standalone page), and \`/settings/policy\` opens the Product learning section. After you save a change, the app keeps you on the section you edited rather than returning to the top.
46688
+ Settings is a left-rail master\u2013detail layout: a grouped rail (General, Team & Access, Billing, AI & Agents, Connections) selects one section at a time, shown in the detail pane. The active section is a URL search param, so \`/settings?section=<id>\` is linkable and survives a refresh. The Connections section is the workspace-level provider surface: Google, Slack, QuickBooks, local runners, seat-scoped agent tokens, and stored secrets appear as provider cards with status, used-for chips, actions, and the shared Access language (**Off / Read / Draft / Act with approval**). Detailed management forms for tokens, channels, runners, provider links, and stored vault entries remain reachable from those cards.
46689
+
46690
+ Legacy hash deep-links still resolve \u2014 \`/settings#integrations\`, \`#channels\`, \`#runners\`, and \`#vault\` redirect to \`?section=connections\` while preserving any status params; other hashes such as \`#members\` and \`#guardrails\` redirect to their matching section. Documented shortcut links resolve to the right section instead of a dead end: \`/settings/integrations\` and \`/readiness\` open the Connections section (connector readiness lives inside it, not on a standalone page), and \`/settings/policy\` opens the Product learning section. After you save a change, the app keeps you on the section you edited rather than returning to the top.
46644
46691
 
46645
46692
  ## What belongs in Settings
46646
46693
 
@@ -46797,7 +46844,7 @@ Every notification should include the seat, cause, evidence, and requested decis
46797
46844
  order: 75,
46798
46845
  title: "Local runner guide",
46799
46846
  summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
46800
- version: "2026-07-04.3",
46847
+ version: "2026-07-04.4",
46801
46848
  public: true,
46802
46849
  audiences: ["human", "cli", "mcp", "in_app_agent"],
46803
46850
  stages: ["staffing", "operating_rhythm"],
@@ -46841,6 +46888,8 @@ Headless and service accounts are supported first-class: when no usable OS keych
46841
46888
 
46842
46889
  Choose the local runtime with \`--runtime auto|claude|codex\` (default \`auto\`). The same short-lived seat-scoped MCP token and server-side guard apply regardless of runtime; results are tagged \`claude-cli\` or \`codex-cli\`.
46843
46890
 
46891
+ For always-on execution, \`runner serve --execute\` heartbeats on its normal interval and also checks for claimable work between heartbeats. The default interactive claim cadence is about one second; set \`RUNNER_INTERACTIVE_CLAIM_MS\` or \`--interactive-claim-ms\` to tune it for a host. Claude-backed turns reuse a per-seat Claude Code resume session when Claude reports a session id, so later turns can run with \`claude --resume <session-id>\` under the local subscription account. The runner stores only the opaque resume id in its owner-only state file; it does not use API-billed managed sessions to make subscription runner turns faster. A resident long-lived Claude/Codex process is still prototype-gated, so do not assume a fixed latency SLA from this guide alone.
46892
+
46844
46893
  The runner bearer secret is stored locally and is never displayed. Each claimed turn receives a short-lived seat-scoped MCP token from {{brand}}, and server-side guards still decide what tools the Seat may use.
46845
46894
 
46846
46895
  ## Interactive AICOS runner turns
@@ -49113,6 +49162,36 @@ function dryRunToolActionLabel(status) {
49113
49162
  return status;
49114
49163
  }
49115
49164
  }
49165
+ var DRY_RUN_LANE_LABELS = {
49166
+ cloud: "cloud lane",
49167
+ runner: "runner lane",
49168
+ mcp_session: "MCP-session lane"
49169
+ };
49170
+ function dryRunLaneLabel(lane) {
49171
+ return DRY_RUN_LANE_LABELS[lane] ?? `${lane} lane`;
49172
+ }
49173
+ function dryRunLaneVerdict(lane, grantedCount, unimplementedCount) {
49174
+ const laneLabel = dryRunLaneLabel(lane);
49175
+ if (unimplementedCount === 0) {
49176
+ return grantedCount === 1 ? `The single granted tool is implemented in the ${laneLabel}.` : `All ${grantedCount} granted tools are implemented in the ${laneLabel}.`;
49177
+ }
49178
+ const verb = unimplementedCount === 1 ? "doesn't" : "don't";
49179
+ return `${unimplementedCount} of ${grantedCount} granted tools ${verb} execute in the ${laneLabel} yet.`;
49180
+ }
49181
+ function dryRunLaneExecutabilityLines(transcript) {
49182
+ const lane = transcript.lane;
49183
+ const grantedTools = asArray(transcript.granted_tools);
49184
+ if (typeof lane !== "string" || grantedTools.length === 0) {
49185
+ return [];
49186
+ }
49187
+ const laneLabel = dryRunLaneLabel(lane);
49188
+ const unimplemented = grantedTools.map((tool) => asRecord(tool)).filter((tool) => tool.executability === "unimplemented_in_lane");
49189
+ const warningLines = unimplemented.map(
49190
+ (tool) => ` warning: ${field(tool, "tool_name")} is granted but unimplemented in the ${laneLabel}`
49191
+ );
49192
+ const verdict = dryRunLaneVerdict(lane, grantedTools.length, unimplemented.length);
49193
+ return ["lane executability:", ...warningLines, ` verdict: ${verdict}`];
49194
+ }
49116
49195
  function formatDryRunTranscript(value) {
49117
49196
  const transcript = asRecord(value);
49118
49197
  const steps = asArray(transcript.steps);
@@ -49129,7 +49208,13 @@ function formatDryRunTranscript(value) {
49129
49208
  const record2 = asRecord(action);
49130
49209
  return ` ${dryRunToolActionLabel(field(record2, "status"))}: ${field(record2, "intended_action")} [${field(record2, "tool_name")}]`;
49131
49210
  });
49132
- return ["rehearsal:", ...lines, ...toolPreview.length > 0 ? ["tool preview:", ...toolPreview] : []].join("\n");
49211
+ const laneLines = dryRunLaneExecutabilityLines(transcript);
49212
+ return [
49213
+ "rehearsal:",
49214
+ ...lines,
49215
+ ...toolPreview.length > 0 ? ["tool preview:", ...toolPreview] : [],
49216
+ ...laneLines
49217
+ ].join("\n");
49133
49218
  }
49134
49219
  function isScalar(value) {
49135
49220
  return value === null || ["string", "number", "boolean"].includes(typeof value);
@@ -51328,7 +51413,7 @@ function parseSemver2(value) {
51328
51413
  // src/runner-serve.ts
51329
51414
  import { execFile as execFileCallback3, spawn } from "node:child_process";
51330
51415
  import { createHash } from "node:crypto";
51331
- import { chmod, mkdir as mkdir2, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
51416
+ import { chmod, mkdir as mkdir2, readFile as readFile4, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
51332
51417
  import { cpus, hostname as hostname3, platform, totalmem, tmpdir } from "node:os";
51333
51418
  import path3 from "node:path";
51334
51419
  import { setTimeout as sleep2 } from "node:timers/promises";
@@ -51373,6 +51458,7 @@ ${usage()}
51373
51458
  do {
51374
51459
  const detectedCapabilities = await detectCapabilities();
51375
51460
  const capabilities = capabilitiesForConfiguredRuntime(detectedCapabilities, config2.runtime);
51461
+ const localRuntime = config2.execute ? effectiveExecutionRuntime(config2.runtime, capabilities) : null;
51376
51462
  try {
51377
51463
  const heartbeat = await post2(fetchImpl, options.appUrl, "/api/runner/heartbeat", {
51378
51464
  capabilities,
@@ -51387,15 +51473,12 @@ ${usage()}
51387
51473
  options.io.stdout.write(`heartbeat ok #${beat} runner_id=${state.runner_id} claude=${capabilities.claude.installed} codex=${capabilities.codex.installed}
51388
51474
  `);
51389
51475
  }
51390
- if (config2.execute) {
51391
- const localRuntime = effectiveExecutionRuntime(config2.runtime, capabilities);
51392
- if (localRuntime) {
51393
- activeSessions += 1;
51394
- try {
51395
- await claimAndExecute(fetchImpl, options.appUrl, state, options.io, localRuntime);
51396
- } finally {
51397
- activeSessions = Math.max(0, activeSessions - 1);
51398
- }
51476
+ if (localRuntime) {
51477
+ activeSessions += 1;
51478
+ try {
51479
+ await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
51480
+ } finally {
51481
+ activeSessions = Math.max(0, activeSessions - 1);
51399
51482
  }
51400
51483
  }
51401
51484
  } catch (error51) {
@@ -51405,11 +51488,27 @@ ${usage()}
51405
51488
  }
51406
51489
  beat += 1;
51407
51490
  if (!config2.once) {
51408
- await sleep2(config2.heartbeatMs);
51491
+ await waitForNextHeartbeat(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
51409
51492
  }
51410
51493
  } while (!config2.once);
51411
51494
  return heartbeatSucceeded && !failed ? 0 : 1;
51412
51495
  }
51496
+ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runtime) {
51497
+ const deadline = Date.now() + config2.heartbeatMs;
51498
+ while (Date.now() < deadline) {
51499
+ const intervalMs = Math.min(config2.interactiveClaimMs, deadline - Date.now());
51500
+ await sleep2(intervalMs);
51501
+ if (!runtime || Date.now() >= deadline) {
51502
+ continue;
51503
+ }
51504
+ try {
51505
+ await claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, { interactiveOnly: true });
51506
+ } catch (error51) {
51507
+ io.stderr.write(`transient runner error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
51508
+ `);
51509
+ }
51510
+ }
51511
+ }
51413
51512
  function detectTelemetry(capabilities, env, activeSessions) {
51414
51513
  const accountAlias = (env.RUNNER_MODEL_ACCOUNT_ALIAS ?? env.ROST_RUNNER_MODEL_ACCOUNT_ALIAS ?? "").trim() || "default";
51415
51514
  const usageState = env.RUNNER_MODEL_ACCOUNT_STATE ?? env.ROST_RUNNER_MODEL_ACCOUNT_STATE ?? "unknown";
@@ -51431,6 +51530,7 @@ function parseServeArgs(args, appUrl2, env) {
51431
51530
  let name = env.RUNNER_NAME ?? `headless-${hostname3()}`;
51432
51531
  let stateFile = env.RUNNER_STATE_FILE ?? path3.join(tmpdir(), `rost-runner-${createHash("sha1").update(appUrl2).digest("hex").slice(0, 8)}.json`);
51433
51532
  let heartbeatMs = Number(env.RUNNER_HEARTBEAT_MS ?? 3e4);
51533
+ let interactiveClaimMs = Number(env.RUNNER_INTERACTIVE_CLAIM_MS ?? 1e3);
51434
51534
  let execute2 = env.RUNNER_EXECUTE === "1";
51435
51535
  let once = false;
51436
51536
  let runtime = parseRuntime(env.RUNNER_RUNTIME ?? "auto");
@@ -51446,6 +51546,9 @@ function parseServeArgs(args, appUrl2, env) {
51446
51546
  } else if (token === "--heartbeat-ms") {
51447
51547
  heartbeatMs = Number(requiredValue(args, index));
51448
51548
  index += 1;
51549
+ } else if (token === "--interactive-claim-ms") {
51550
+ interactiveClaimMs = Number(requiredValue(args, index));
51551
+ index += 1;
51449
51552
  } else if (token === "--user-code") {
51450
51553
  userCode = requiredValue(args, index);
51451
51554
  index += 1;
@@ -51463,7 +51566,11 @@ function parseServeArgs(args, appUrl2, env) {
51463
51566
  if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3) {
51464
51567
  throw new Error("--heartbeat-ms must be an integer >= 1000.");
51465
51568
  }
51466
- return { name, stateFile, heartbeatMs, execute: execute2, once, runtime, ...userCode ? { userCode } : {} };
51569
+ if (!Number.isSafeInteger(interactiveClaimMs) || interactiveClaimMs < 250) {
51570
+ throw new Error("--interactive-claim-ms must be an integer >= 250.");
51571
+ }
51572
+ interactiveClaimMs = Math.min(interactiveClaimMs, heartbeatMs);
51573
+ return { name, stateFile, heartbeatMs, interactiveClaimMs, execute: execute2, once, runtime, ...userCode ? { userCode } : {} };
51467
51574
  }
51468
51575
  function parseRuntime(value) {
51469
51576
  if (value === "auto" || value === "claude" || value === "codex") {
@@ -51545,10 +51652,7 @@ async function pairIfNeeded(client, fetchImpl, appUrl2, config2, io) {
51545
51652
  ...typeof tenantId === "string" ? { tenant_id: tenantId } : config2.expectedTenantId ? { tenant_id: config2.expectedTenantId } : {},
51546
51653
  name: typeof claim.json.name === "string" ? claim.json.name : config2.name
51547
51654
  };
51548
- await mkdir2(path3.dirname(config2.stateFile), { recursive: true, mode: 448 });
51549
- await writeFile2(config2.stateFile, `${JSON.stringify(state)}
51550
- `, { mode: 384 });
51551
- await chmod(config2.stateFile, 384);
51655
+ await saveState(config2.stateFile, state);
51552
51656
  io.stdout.write(`paired runner_id=${runnerId} state_file=${config2.stateFile}
51553
51657
  `);
51554
51658
  return state;
@@ -51564,7 +51668,8 @@ async function loadState(filePath, expectedTenantId) {
51564
51668
  runner_id: parsed.runner_id,
51565
51669
  runner_secret: parsed.runner_secret,
51566
51670
  ...typeof parsed.tenant_id === "string" ? { tenant_id: parsed.tenant_id } : {},
51567
- ...typeof parsed.name === "string" ? { name: parsed.name } : {}
51671
+ ...typeof parsed.name === "string" ? { name: parsed.name } : {},
51672
+ ...isRuntimeSessionMap(parsed.runtime_sessions) ? { runtime_sessions: parsed.runtime_sessions } : {}
51568
51673
  };
51569
51674
  }
51570
51675
  } catch {
@@ -51572,6 +51677,31 @@ async function loadState(filePath, expectedTenantId) {
51572
51677
  }
51573
51678
  return null;
51574
51679
  }
51680
+ function isRuntimeSessionMap(value) {
51681
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
51682
+ return false;
51683
+ }
51684
+ return Object.values(value).every((entry) => {
51685
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
51686
+ return false;
51687
+ }
51688
+ const record2 = entry;
51689
+ return (record2.claude_session_id === void 0 || typeof record2.claude_session_id === "string") && (record2.updated_at === void 0 || typeof record2.updated_at === "string");
51690
+ });
51691
+ }
51692
+ async function saveState(filePath, state) {
51693
+ await mkdir2(path3.dirname(filePath), { recursive: true, mode: 448 });
51694
+ const tempPath = `${filePath}.${process.pid}.${createHash("sha1").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 8)}.tmp`;
51695
+ try {
51696
+ await writeFile2(tempPath, `${JSON.stringify(state)}
51697
+ `, { mode: 384 });
51698
+ await chmod(tempPath, 384);
51699
+ await rename(tempPath, filePath);
51700
+ } catch (error51) {
51701
+ await rm2(tempPath, { force: true });
51702
+ throw error51;
51703
+ }
51704
+ }
51575
51705
  async function startPairing(client, config2) {
51576
51706
  const response = await client.execute("runner.pairing.start", { name: config2.name, platform: process.platform });
51577
51707
  const userCode = response.output && typeof response.output === "object" ? response.output.user_code : void 0;
@@ -51611,11 +51741,17 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
51611
51741
  }
51612
51742
  return { status: response.status, json: json2 };
51613
51743
  }
51614
- async function claimAndExecute(fetchImpl, appUrl2, state, io, runtime) {
51615
- const claimed = await post2(fetchImpl, appUrl2, "/api/runner/work-orders/claim", {}, state.runner_secret);
51744
+ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, options = {}) {
51745
+ const claimed = await post2(
51746
+ fetchImpl,
51747
+ appUrl2,
51748
+ "/api/runner/work-orders/claim",
51749
+ options.interactiveOnly ? { interactive_only: true } : {},
51750
+ state.runner_secret
51751
+ );
51616
51752
  const turnExecution = claimed.json.turn_execution;
51617
51753
  if (claimed.status === 200 && turnExecution && typeof turnExecution === "object") {
51618
- await executeClaimedUnit(fetchImpl, appUrl2, state, io, runtime, {
51754
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
51619
51755
  kind: "turn_execution",
51620
51756
  idField: "turn_execution_id",
51621
51757
  item: turnExecution,
@@ -51629,7 +51765,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, io, runtime) {
51629
51765
  if (claimed.status !== 200 || !workOrder || typeof workOrder !== "object") {
51630
51766
  return;
51631
51767
  }
51632
- await executeClaimedUnit(fetchImpl, appUrl2, state, io, runtime, {
51768
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
51633
51769
  kind: "work_order",
51634
51770
  idField: "work_order_id",
51635
51771
  item: workOrder,
@@ -51638,7 +51774,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, io, runtime) {
51638
51774
  logRef: (id) => `runner-local:${id}`
51639
51775
  });
51640
51776
  }
51641
- async function executeClaimedUnit(fetchImpl, appUrl2, state, io, runtime, input) {
51777
+ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, input) {
51642
51778
  const id = input.item.id;
51643
51779
  if (typeof id !== "string") {
51644
51780
  return;
@@ -51648,10 +51784,22 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, io, runtime, input)
51648
51784
  throw new Error(`${claimedUnitLabel(input.kind)} start failed ${started.status}: ${redactForLog(JSON.stringify(started.json))}`);
51649
51785
  }
51650
51786
  const turnRuntime = runtimeForClaimedUnit(input.item, runtime, input.kind);
51651
- const result = turnRuntime ? await runLocalTurn(appUrl2, input.item, turnRuntime, input.kind) : {
51787
+ const result = turnRuntime ? await runLocalTurn(appUrl2, input.item, turnRuntime, input.kind, state) : {
51652
51788
  ok: false,
51653
51789
  summary: `Runner runtime ${runtime} cannot execute requested ${String(input.item.brain ?? "unknown")} turn.`
51654
51790
  };
51791
+ if (input.kind === "turn_execution" && turnRuntime === "claude") {
51792
+ try {
51793
+ if (result.ok && result.claudeSessionId) {
51794
+ await persistClaudeSessionId(config2.stateFile, state, input.item, result.claudeSessionId);
51795
+ } else if (!result.ok && result.resumedFromSessionId) {
51796
+ await clearClaudeSessionId(config2.stateFile, state, input.item);
51797
+ }
51798
+ } catch (error51) {
51799
+ io.stderr.write(`resume cache update failed (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
51800
+ `);
51801
+ }
51802
+ }
51655
51803
  const reported = await post2(fetchImpl, appUrl2, input.resultPath(id), {
51656
51804
  status: result.ok ? "succeeded" : "failed",
51657
51805
  summary: result.summary,
@@ -51676,16 +51824,16 @@ function runtimeForClaimedUnit(item, fallback, kind) {
51676
51824
  }
51677
51825
  return fallback === "claude" ? "claude" : null;
51678
51826
  }
51679
- function runLocalTurn(appUrl2, workOrder, runtime, kind) {
51680
- return runtime === "codex" ? runCodexTurn(appUrl2, workOrder, kind) : runClaudeTurn(appUrl2, workOrder, kind);
51827
+ function runLocalTurn(appUrl2, workOrder, runtime, kind, state) {
51828
+ return runtime === "codex" ? runCodexTurn(appUrl2, workOrder, kind) : runClaudeTurn(appUrl2, workOrder, kind, state);
51681
51829
  }
51682
- function runClaudeTurn(appUrl2, workOrder, kind) {
51683
- return spawnRunnerTurn(appUrl2, workOrder, "claude", kind);
51830
+ function runClaudeTurn(appUrl2, workOrder, kind, state) {
51831
+ return spawnRunnerTurn(appUrl2, workOrder, "claude", kind, state);
51684
51832
  }
51685
51833
  function runCodexTurn(appUrl2, workOrder, kind) {
51686
51834
  return spawnRunnerTurn(appUrl2, workOrder, "codex", kind);
51687
51835
  }
51688
- function buildTurnCommand(runtime, prompt, configPath, kind) {
51836
+ function buildTurnCommand(runtime, prompt, configPath, kind, resumeSessionId) {
51689
51837
  if (runtime === "codex") {
51690
51838
  return {
51691
51839
  command: "codex",
@@ -51703,11 +51851,12 @@ function buildTurnCommand(runtime, prompt, configPath, kind) {
51703
51851
  "--allowedTools",
51704
51852
  (kind === "turn_execution" ? RUNNER_AICOS_TURN_TOOLS : RUNNER_WORK_ORDER_TOOLS).join(","),
51705
51853
  "--output-format",
51706
- kind === "turn_execution" ? "json" : "text"
51854
+ kind === "turn_execution" ? "json" : "text",
51855
+ ...resumeSessionId ? ["--resume", resumeSessionId] : []
51707
51856
  ]
51708
51857
  };
51709
51858
  }
51710
- async function spawnRunnerTurn(appUrl2, workOrder, runtime, kind) {
51859
+ async function spawnRunnerTurn(appUrl2, workOrder, runtime, kind, state) {
51711
51860
  const mcp = typeof workOrder.mcp === "object" && workOrder.mcp !== null ? workOrder.mcp : {};
51712
51861
  const token = typeof mcp.token === "string" ? mcp.token : "";
51713
51862
  const url2 = typeof mcp.url === "string" ? `${appUrl2.replace(/\/+$/, "")}${mcp.url}` : `${appUrl2.replace(/\/+$/, "")}/mcp`;
@@ -51741,7 +51890,9 @@ async function spawnRunnerTurn(appUrl2, workOrder, runtime, kind) {
51741
51890
  await writeFile2(configPath, `${mcpConfig}
51742
51891
  `, { mode: 384 });
51743
51892
  await chmod(configPath, 384);
51744
- const { command, args } = buildTurnCommand(runtime, prompt, configPath, kind);
51893
+ const resumeSessionId = runtime === "claude" ? getClaudeResumeSessionId(state, workOrder) : void 0;
51894
+ const { command, args } = buildTurnCommand(runtime, prompt, configPath, kind, resumeSessionId);
51895
+ const expectSessionEnvelope = runtime === "claude" && kind === "turn_execution";
51745
51896
  return new Promise((resolve2) => {
51746
51897
  let settled = false;
51747
51898
  const finish = (result) => {
@@ -51750,7 +51901,7 @@ async function spawnRunnerTurn(appUrl2, workOrder, runtime, kind) {
51750
51901
  }
51751
51902
  settled = true;
51752
51903
  void rm2(configPath, { force: true });
51753
- resolve2(result);
51904
+ resolve2(resumeSessionId ? { ...result, resumedFromSessionId: resumeSessionId } : result);
51754
51905
  };
51755
51906
  const child = spawn(command, args, {
51756
51907
  cwd: tmpdir(),
@@ -51770,7 +51921,12 @@ async function spawnRunnerTurn(appUrl2, workOrder, runtime, kind) {
51770
51921
  });
51771
51922
  child.on("close", (code) => {
51772
51923
  clearTimeout(timer);
51773
- finish({ ok: code === 0, summary: extractRunnerSummary(redactForLog((out || err || "completed").trim())).slice(0, 12e3) });
51924
+ const rawOutput = (out || err || "completed").trim();
51925
+ finish({
51926
+ ok: code === 0,
51927
+ summary: extractRunnerSummary(redactForLog(rawOutput)).slice(0, 12e3),
51928
+ ...expectSessionEnvelope ? extractClaudeSessionId(rawOutput) : {}
51929
+ });
51774
51930
  });
51775
51931
  child.on("error", (error51) => {
51776
51932
  clearTimeout(timer);
@@ -51796,8 +51952,83 @@ function extractRunnerSummary(output) {
51796
51952
  }
51797
51953
  return trimmed;
51798
51954
  }
51955
+ function extractClaudeSessionId(output) {
51956
+ const trimmed = output.trim();
51957
+ if (!trimmed.startsWith("{")) {
51958
+ return {};
51959
+ }
51960
+ try {
51961
+ const parsed = JSON.parse(trimmed);
51962
+ const sessionId = parsed.session_id ?? parsed.sessionId;
51963
+ return typeof sessionId === "string" && sessionId.trim().length > 0 ? { claudeSessionId: sessionId.trim() } : {};
51964
+ } catch {
51965
+ return {};
51966
+ }
51967
+ }
51968
+ function runtimeSessionKey(state, workOrder) {
51969
+ const seatId = extractSeatId(workOrder);
51970
+ const userId = extractTurnUserId(workOrder);
51971
+ const sessionId = extractTurnSessionId(workOrder);
51972
+ if (!state?.tenant_id || !seatId || !userId || !sessionId) {
51973
+ return null;
51974
+ }
51975
+ return `${state.tenant_id}:seat:${seatId}:user:${userId}:session:${sessionId}:claude`;
51976
+ }
51977
+ function extractTurnUserId(workOrder) {
51978
+ const userId = workOrder.user_id;
51979
+ return typeof userId === "string" && userId.length > 0 ? userId : null;
51980
+ }
51981
+ function extractTurnSessionId(workOrder) {
51982
+ const topLevel = workOrder.session_id;
51983
+ if (typeof topLevel === "string" && topLevel.length > 0) {
51984
+ return topLevel;
51985
+ }
51986
+ const aicos = typeof workOrder.aicos === "object" && workOrder.aicos !== null ? workOrder.aicos : null;
51987
+ const sessionId = aicos?.session_id;
51988
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
51989
+ }
51990
+ function extractSeatId(workOrder) {
51991
+ const seat = typeof workOrder.seat === "object" && workOrder.seat !== null ? workOrder.seat : null;
51992
+ if (typeof seat?.id === "string" && seat.id.length > 0) {
51993
+ return seat.id;
51994
+ }
51995
+ const charterRef = typeof workOrder.charter_ref === "object" && workOrder.charter_ref !== null ? workOrder.charter_ref : null;
51996
+ return typeof charterRef?.seat_id === "string" && charterRef.seat_id.length > 0 ? charterRef.seat_id : null;
51997
+ }
51998
+ function getClaudeResumeSessionId(state, workOrder) {
51999
+ const key = runtimeSessionKey(state, workOrder);
52000
+ if (!key) {
52001
+ return void 0;
52002
+ }
52003
+ const sessionId = state?.runtime_sessions?.[key]?.claude_session_id;
52004
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : void 0;
52005
+ }
52006
+ async function persistClaudeSessionId(stateFile, state, workOrder, claudeSessionId) {
52007
+ const key = runtimeSessionKey(state, workOrder);
52008
+ if (!key) {
52009
+ return;
52010
+ }
52011
+ state.runtime_sessions = {
52012
+ ...state.runtime_sessions ?? {},
52013
+ [key]: {
52014
+ claude_session_id: claudeSessionId,
52015
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
52016
+ }
52017
+ };
52018
+ await saveState(stateFile, state);
52019
+ }
52020
+ async function clearClaudeSessionId(stateFile, state, workOrder) {
52021
+ const key = runtimeSessionKey(state, workOrder);
52022
+ if (!key || !state.runtime_sessions?.[key]) {
52023
+ return;
52024
+ }
52025
+ const nextSessions = { ...state.runtime_sessions };
52026
+ delete nextSessions[key];
52027
+ state.runtime_sessions = nextSessions;
52028
+ await saveState(stateFile, state);
52029
+ }
51799
52030
  function usage() {
51800
- return "Usage: rost runner serve [--name <text>] [--state-file <path>] [--heartbeat-ms <n>] [--user-code <code>] [--runtime auto|claude|codex] [--once] [--execute]";
52031
+ return "Usage: rost runner serve [--name <text>] [--state-file <path>] [--heartbeat-ms <n>] [--interactive-claim-ms <n>] [--user-code <code>] [--runtime auto|claude|codex] [--once] [--execute]";
51801
52032
  }
51802
52033
 
51803
52034
  // src/runner-service.ts