@rosthq/cli 0.7.56 → 0.7.63

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
@@ -13258,6 +13258,7 @@ var require_main3 = __commonJS({
13258
13258
 
13259
13259
  // src/index.ts
13260
13260
  import { realpathSync as realpathSync3 } from "node:fs";
13261
+ import { createInterface } from "node:readline/promises";
13261
13262
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
13262
13263
 
13263
13264
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -37800,6 +37801,104 @@ import { readFileSync } from "node:fs";
37800
37801
  import { dirname, join } from "node:path";
37801
37802
  import { fileURLToPath } from "node:url";
37802
37803
 
37804
+ // ../../packages/protocol/src/cron.ts
37805
+ var uintPattern = /^\d+$/;
37806
+ var CRON_FIELD_SPECS = [
37807
+ { name: "minute", min: 0, max: 59 },
37808
+ { name: "hour", min: 0, max: 23 },
37809
+ { name: "day-of-month", min: 1, max: 31 },
37810
+ { name: "month", min: 1, max: 12 },
37811
+ { name: "day-of-week", min: 0, max: 7 }
37812
+ ];
37813
+ var CRON_FIELD_COUNT = CRON_FIELD_SPECS.length;
37814
+ function parseCronField(raw, spec) {
37815
+ const parts = raw.split(",");
37816
+ const terms = [];
37817
+ for (const part of parts) {
37818
+ const token = part.trim();
37819
+ if (token.length === 0) {
37820
+ return { ok: false, reason: "empty value in list" };
37821
+ }
37822
+ if (token === "*") {
37823
+ terms.push({ kind: "all" });
37824
+ continue;
37825
+ }
37826
+ if (token.startsWith("*/")) {
37827
+ const stepText = token.slice(2);
37828
+ if (!uintPattern.test(stepText)) {
37829
+ return { ok: false, reason: `"${token}" is not a valid step` };
37830
+ }
37831
+ const step = Number(stepText);
37832
+ if (step < 1 || step > spec.max) {
37833
+ return { ok: false, reason: `step in "${token}" must be between 1 and ${spec.max}` };
37834
+ }
37835
+ terms.push({ kind: "step", step });
37836
+ continue;
37837
+ }
37838
+ if (token.includes("-")) {
37839
+ const segments = token.split("-");
37840
+ if (segments.length !== 2 || !uintPattern.test(segments[0] ?? "") || !uintPattern.test(segments[1] ?? "")) {
37841
+ return { ok: false, reason: `"${token}" is not a valid range` };
37842
+ }
37843
+ const start = Number(segments[0]);
37844
+ const end = Number(segments[1]);
37845
+ if (start < spec.min || end > spec.max || start > end) {
37846
+ return { ok: false, reason: `range "${token}" must be within ${spec.min}-${spec.max} and ascending` };
37847
+ }
37848
+ terms.push({ kind: "range", start, end });
37849
+ continue;
37850
+ }
37851
+ if (uintPattern.test(token)) {
37852
+ const value = Number(token);
37853
+ if (value < spec.min || value > spec.max) {
37854
+ return { ok: false, reason: `"${token}" must be between ${spec.min} and ${spec.max}` };
37855
+ }
37856
+ terms.push({ kind: "value", value });
37857
+ continue;
37858
+ }
37859
+ return { ok: false, reason: `"${token}" is not a supported cron token` };
37860
+ }
37861
+ return { ok: true, field: terms };
37862
+ }
37863
+ function parseCronExpression(expression) {
37864
+ const trimmed = expression.trim();
37865
+ if (trimmed.length === 0) {
37866
+ return { ok: false, message: "Schedule must not be empty." };
37867
+ }
37868
+ if (/[\r\n\u2028\u2029]/.test(trimmed)) {
37869
+ return { ok: false, message: "Schedule must be a single-line cron expression." };
37870
+ }
37871
+ if (/[^0-9*/,\t\x20-]/.test(trimmed)) {
37872
+ return { ok: false, message: "Schedule may only contain digits, spaces, and the characters * / , -" };
37873
+ }
37874
+ const rawFields = trimmed.split(/\s+/);
37875
+ if (rawFields.length !== CRON_FIELD_COUNT) {
37876
+ return {
37877
+ ok: false,
37878
+ message: `Schedule must have exactly ${CRON_FIELD_COUNT} fields (minute hour day-of-month month day-of-week); got ${rawFields.length}.`
37879
+ };
37880
+ }
37881
+ const fields = [];
37882
+ for (let i = 0; i < CRON_FIELD_COUNT; i += 1) {
37883
+ const spec = CRON_FIELD_SPECS[i];
37884
+ const result = parseCronField(rawFields[i], spec);
37885
+ if (!result.ok) {
37886
+ return { ok: false, message: `Invalid ${spec.name} field: ${result.reason}.` };
37887
+ }
37888
+ fields.push(result.field);
37889
+ }
37890
+ return { ok: true, cron: { fields } };
37891
+ }
37892
+ var cronExpressionSchema = external_exports.string().trim().min(1).max(120).superRefine((value, ctx) => {
37893
+ if (value.length > 120) {
37894
+ return;
37895
+ }
37896
+ const result = parseCronExpression(value);
37897
+ if (!result.ok) {
37898
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: result.message });
37899
+ }
37900
+ });
37901
+
37803
37902
  // ../../packages/protocol/src/agent-model-config.ts
37804
37903
  var agentModelProviderSchema = external_exports.enum(["anthropic"]);
37805
37904
  var agentModelEffortSchema = external_exports.enum(["low", "medium", "high", "xhigh", "max"]);
@@ -37922,7 +38021,7 @@ var dryRunTranscriptSchema = external_exports.object({
37922
38021
  }).strict();
37923
38022
 
37924
38023
  // ../../packages/protocol/src/scrub.ts
37925
- var SECRET_SHAPED_VALUE = /(sk-ant-|sk-proj-|sk-live-|sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|BEGIN [A-Z ]*PRIVATE KEY|api[_-]?key\s*=|secret\s*=|password\s*=|authorization:\s*bearer\s+|x-api-key\s*:|api[-_ ]?key\s*:\s*\S{3,}|secret\s*:\s*\S{3,}|password\s*:\s*\S{3,}|token\s*:\s*\S{3,})/i;
38024
+ var SECRET_SHAPED_VALUE = /(rost_mcp_[A-Za-z0-9_-]{20,}|sk-ant-|sk-proj-|sk-live-|sk-[A-Za-z0-9_-]{12,}|gh[opsur]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{20,}|xox[baoprs]-[A-Za-z0-9-]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16}|BEGIN [A-Z ]*PRIVATE KEY|api[_-]?key\s*=|secret\s*=|password\s*=|authorization:\s*bearer\s+|(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}|x-api-key\s*:|api[-_ ]?key\s*:\s*\S{3,}|secret\s*:\s*\S{3,}|password\s*:\s*\S{3,}|token\s*:\s*\S{3,})/i;
37926
38025
  var SECRET_KEY = /(api[_-]?key|secret|password|token|authorization|credential|private[_-]?key)/i;
37927
38026
  function hasSecretShapedContent(value) {
37928
38027
  if (typeof value === "string") {
@@ -38676,7 +38775,7 @@ var agentLifecycleSchema = external_exports.enum([
38676
38775
  "paused",
38677
38776
  "decommissioned"
38678
38777
  ]);
38679
- var cronSchema = external_exports.string().trim().min(1).max(120).regex(/^[^\n\r]+$/, "Schedule must be a single-line cron expression.");
38778
+ var cronSchema = cronExpressionSchema;
38680
38779
  var agentTemplateListInputSchema = external_exports.object({}).strict();
38681
38780
  var agentTemplateSummarySchema = external_exports.object({
38682
38781
  slug: external_exports.string().min(1),
@@ -38784,6 +38883,11 @@ var agentSetupStateSchema = external_exports.object({
38784
38883
  steward_seat_id: uuidSchema2.nullable(),
38785
38884
  steward_chain_resolves_to_human: external_exports.boolean(),
38786
38885
  has_occupancy: external_exports.boolean(),
38886
+ // DER-1529: the seat agent's run-time skill-discovery toggle. VISIBILITY only —
38887
+ // it never grants tools (invariant #6). Always present (the column is NOT NULL
38888
+ // default false) so agent_setup.get / .update round-trip the current value back
38889
+ // to any client; conservative default false (#10).
38890
+ skill_discovery_enabled: external_exports.boolean(),
38787
38891
  tool_decisions: external_exports.array(agentSetupToolDecisionSchema),
38788
38892
  available_skill_recommendations: external_exports.array(agentSetupSkillRecommendationSchema),
38789
38893
  assigned_skills: external_exports.array(skillAssignmentSummarySchema),
@@ -38815,9 +38919,12 @@ var agentSetupUpdateInputSchema = external_exports.object({
38815
38919
  // DER-787 (H1): set the structured model selection for this agent.
38816
38920
  model_config: agentModelConfigSchema.optional(),
38817
38921
  answers: external_exports.array(external_exports.string().min(1).max(2e3)).max(20).optional(),
38818
- tool_decisions: external_exports.array(agentSetupToolUpdateSchema).max(50).optional()
38922
+ tool_decisions: external_exports.array(agentSetupToolUpdateSchema).max(50).optional(),
38923
+ // DER-1529: toggle run-time skill discovery (visibility only; never grants
38924
+ // tools). Settable any time, including on a live agent.
38925
+ skill_discovery_enabled: external_exports.boolean().optional()
38819
38926
  }).strict().refine(
38820
- (value) => value.parent_seat_id !== void 0 || value.steward_seat_id !== void 0 || value.lane !== void 0 || value.schedule_cron !== void 0 || value.model_config !== void 0 || value.answers !== void 0 || value.tool_decisions !== void 0,
38927
+ (value) => value.parent_seat_id !== void 0 || value.steward_seat_id !== void 0 || value.lane !== void 0 || value.schedule_cron !== void 0 || value.model_config !== void 0 || value.answers !== void 0 || value.tool_decisions !== void 0 || value.skill_discovery_enabled !== void 0,
38821
38928
  "Provide at least one field to update."
38822
38929
  );
38823
38930
  var agentUpdateScheduleInputSchema = external_exports.object({
@@ -39211,6 +39318,13 @@ var AGENT_POLICY_DEFAULT = {
39211
39318
  max_autonomous_risk: AGENT_POLICY_PROFILE_CEILINGS.balanced
39212
39319
  };
39213
39320
 
39321
+ // ../../packages/protocol/src/confirmation-policy.ts
39322
+ var CLI_CONFIRMATION_MODES = ["trusted_operator", "careful"];
39323
+ var cliConfirmationModeSchema = external_exports.enum(CLI_CONFIRMATION_MODES);
39324
+ var storedConfirmationPolicySchema = external_exports.object({
39325
+ cli_mode: cliConfirmationModeSchema
39326
+ }).strict();
39327
+
39214
39328
  // ../../packages/protocol/src/aicos.ts
39215
39329
  var uuidSchema5 = external_exports.string().uuid();
39216
39330
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.unknown());
@@ -39457,17 +39571,38 @@ var AICOS_ATTACHMENT_CONTENT_TYPES = ["image/png", "image/jpeg", "image/webp", "
39457
39571
  var AICOS_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
39458
39572
  var AICOS_MAX_ATTACHMENTS_PER_TURN = 4;
39459
39573
  var aicosAttachmentContentTypeSchema = external_exports.enum(AICOS_ATTACHMENT_CONTENT_TYPES);
39574
+ var AICOS_ATTACHMENT_READINESS = ["ready", "unavailable", "expired"];
39575
+ var aicosAttachmentReadinessSchema = external_exports.enum(AICOS_ATTACHMENT_READINESS);
39460
39576
  var aicosAttachmentSchema = external_exports.object({
39461
39577
  id: uuidSchema5,
39462
39578
  contentType: aicosAttachmentContentTypeSchema,
39463
39579
  sizeBytes: external_exports.number().int().positive(),
39464
39580
  width: external_exports.number().int().positive().nullable(),
39465
39581
  height: external_exports.number().int().positive().nullable(),
39466
- filename: external_exports.string().max(255).nullable()
39582
+ filename: external_exports.string().max(255).nullable(),
39583
+ // DER-1470: null/absent ref_status reads as "ready" (usable) for back-compat.
39584
+ readiness: aicosAttachmentReadinessSchema.default("ready")
39467
39585
  }).strict();
39468
39586
  var aicosAttachmentUploadResultSchema = external_exports.object({
39469
39587
  attachment: aicosAttachmentSchema
39470
39588
  }).strict();
39589
+ var aicosRunnerAttachmentRefSchema = external_exports.object({
39590
+ id: uuidSchema5,
39591
+ content_type: aicosAttachmentContentTypeSchema,
39592
+ size_bytes: external_exports.number().int().positive(),
39593
+ width: external_exports.number().int().positive().nullable(),
39594
+ height: external_exports.number().int().positive().nullable(),
39595
+ filename: external_exports.string().max(255).nullable(),
39596
+ signed_url: external_exports.string().url().nullable(),
39597
+ signed_url_expires_in_seconds: external_exports.number().int().positive().nullable(),
39598
+ expires_at: external_exports.string().datetime({ offset: true }).nullable(),
39599
+ readiness: aicosAttachmentReadinessSchema
39600
+ }).strict();
39601
+ var AICOS_RUNNER_ATTACHMENT_OUTCOMES = ["used", "expired", "load_failed"];
39602
+ var aicosRunnerAttachmentOutcomeSchema = external_exports.object({
39603
+ id: uuidSchema5,
39604
+ outcome: external_exports.enum(AICOS_RUNNER_ATTACHMENT_OUTCOMES)
39605
+ }).strict();
39471
39606
  var aicosMessageSchema = external_exports.object({
39472
39607
  id: uuidSchema5,
39473
39608
  sessionId: uuidSchema5,
@@ -39509,6 +39644,7 @@ var aicosAgentBuilderMetadataSchema = external_exports.object({
39509
39644
  agentBuilder: agentBuilderCompilationSchema
39510
39645
  }).strict();
39511
39646
  var aicosTurnRequestSchema = external_exports.object({
39647
+ requestId: uuidSchema5,
39512
39648
  sessionId: uuidSchema5.nullable().optional(),
39513
39649
  purpose: aicosConversationPurposeSchema.default("general"),
39514
39650
  // Empty is permitted only when attachments are present (an image-only turn);
@@ -41176,6 +41312,9 @@ var MCP_ONBOARDING_TOOL_NAMES = {
41176
41312
  setCompass: `${MCP_TOOL_PREFIX}_set_compass`,
41177
41313
  staffSeat: `${MCP_TOOL_PREFIX}_staff_seat`
41178
41314
  };
41315
+ var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
41316
+ var FORGE_TURN_PHASE_OVERHEAD_SECONDS = 20 * 60;
41317
+ var SEAT_MCP_CLAIM_TOKEN_TTL_SECONDS = (FORGE_TURN_HARD_CEILING_SECONDS + FORGE_TURN_PHASE_OVERHEAD_SECONDS) * 2;
41179
41318
 
41180
41319
  // ../../packages/protocol/src/mcp-onboarding.ts
41181
41320
  var MCP_TOKEN_ABSOLUTE_MAX_EXPIRY_MS = (MCP_TOKEN_NO_EXPIRY_SENTINEL_DAYS + 1) * 24 * 60 * 60 * 1e3;
@@ -41363,7 +41502,15 @@ var pendingConfirmationSchema = external_exports.object({
41363
41502
  }).strict()
41364
41503
  }).strict();
41365
41504
  var confirmationApproveInputSchema = external_exports.object({
41366
- confirmation_id: external_exports.string().min(1)
41505
+ confirmation_id: external_exports.string().min(1),
41506
+ // DER-1490 — the auditable "reviewed" signal. The CLI sets it true ONLY after
41507
+ // rendering the confirmation gate to (and, at an interactive TTY, getting an
41508
+ // explicit `y` from) the human. Absent = a silent auto-replay; the server refuses
41509
+ // that path under the tenant's `careful` mode, and for `dangerous` risk in either
41510
+ // mode (see requiresReviewSignal). It is an explicit auditable field, not a TTY
41511
+ // heuristic — the server cannot cryptographically distinguish a human `y` from an
41512
+ // agent replaying a live session token (both arrive as source cli, actor.kind user).
41513
+ reviewed: external_exports.boolean().optional()
41367
41514
  }).strict();
41368
41515
  var confirmationRejectInputSchema = external_exports.object({
41369
41516
  confirmation_id: external_exports.string().min(1),
@@ -41910,7 +42057,7 @@ var fleetDigestAgentSchema = fleetOverviewRowSchema.extend({
41910
42057
  spend_7d_usd: external_exports.string(),
41911
42058
  spend_30d_usd: external_exports.string(),
41912
42059
  cost_drift_percent: external_exports.number().int().nullable(),
41913
- health_status: external_exports.enum(["needs_review", "stale", "setup_gap", "healthy"]),
42060
+ health_status: external_exports.enum(["needs_review", "stale", "setup_gap", "awaiting_data", "healthy"]),
41914
42061
  health_reasons: external_exports.array(external_exports.string()),
41915
42062
  recent_failed_runs: external_exports.array(fleetDigestRunSchema),
41916
42063
  unresolved_errors: external_exports.array(fleetDigestErrorSchema),
@@ -42141,6 +42288,11 @@ var errorLogSupersedeOutputSchema = external_exports.object({
42141
42288
  resolved_at: isoDateTime3
42142
42289
  }).strict();
42143
42290
 
42291
+ // ../../packages/protocol/src/run-summary.ts
42292
+ var runSummaryOutputSchema = external_exports.object({
42293
+ summary: external_exports.string().min(1).max(140)
42294
+ });
42295
+
42144
42296
  // ../../packages/protocol/src/runner-settings.ts
42145
42297
  var uuid8 = external_exports.string().uuid();
42146
42298
  var isoDateTime4 = external_exports.string().datetime({ offset: true });
@@ -42150,6 +42302,7 @@ var runnerExecutionStateSchema = external_exports.enum([
42150
42302
  "processing",
42151
42303
  "idle_unproven",
42152
42304
  "queued_unclaimed",
42305
+ "model_account_exhausted",
42153
42306
  "missing_runtime",
42154
42307
  "not_connected",
42155
42308
  "revoked",
@@ -42165,8 +42318,9 @@ var runnerExecutionReadinessSchema = external_exports.object({
42165
42318
  recent_completed_at: isoDateTime4.nullable(),
42166
42319
  recent_failed_at: isoDateTime4.nullable()
42167
42320
  }).strict();
42321
+ var runnerSandboxPostureSchema = external_exports.enum(["guard", "strict", "off", "none", "unknown"]);
42168
42322
  var runnerPostureSchema = external_exports.object({
42169
- sandbox: external_exports.enum(["guard", "strict", "off", "none", "unknown"]),
42323
+ sandbox: runnerSandboxPostureSchema,
42170
42324
  network: external_exports.enum(["allow", "deny", "unknown"]),
42171
42325
  capability_set: external_exports.array(external_exports.string())
42172
42326
  }).strict();
@@ -42408,6 +42562,17 @@ var agentPolicyUpdateInputSchema = external_exports.object({
42408
42562
  });
42409
42563
  }
42410
42564
  });
42565
+ var confirmationPolicyDtoSchema = external_exports.object({
42566
+ cli_mode: cliConfirmationModeSchema,
42567
+ explicit: external_exports.boolean()
42568
+ }).strict();
42569
+ var confirmationPolicyGetInputSchema = external_exports.object({}).strict();
42570
+ var confirmationPolicyGetOutputSchema = external_exports.object({
42571
+ confirmation_policy: confirmationPolicyDtoSchema
42572
+ }).strict();
42573
+ var confirmationPolicyUpdateInputSchema = external_exports.object({
42574
+ cli_mode: cliConfirmationModeSchema
42575
+ }).strict();
42411
42576
  var tenantSpendStatusSchema = external_exports.object({
42412
42577
  spend_usd: external_exports.string(),
42413
42578
  soft_cap_usd: external_exports.string().nullable(),
@@ -42903,6 +43068,20 @@ var softwareTerminationBoundSchema = external_exports.object({
42903
43068
  max_wall_clock_seconds: external_exports.number().int().positive().max(7 * 24 * 60 * 60).optional(),
42904
43069
  cost_budget_usd: external_exports.number().nonnegative().max(1e6).optional()
42905
43070
  }).strict();
43071
+ var softwarePhaseSchedulerStateSchema = external_exports.object({
43072
+ status: external_exports.enum(["queued", "parked", "not_configured"]),
43073
+ reason: external_exports.enum([
43074
+ "phase_run_not_found",
43075
+ "missing_owning_seat",
43076
+ "missing_runner_agent",
43077
+ "runner_not_paired",
43078
+ "work_order_expired",
43079
+ "runner_offline"
43080
+ ]).nullable(),
43081
+ message: external_exports.string(),
43082
+ remediation_href: external_exports.string().nullable(),
43083
+ work_order_id: uuid10.nullable()
43084
+ });
42906
43085
  var softwareBuildRequestSummarySchema = external_exports.object({
42907
43086
  id: uuid10,
42908
43087
  software_project_id: uuid10,
@@ -42917,7 +43096,8 @@ var softwareBuildRequestSummarySchema = external_exports.object({
42917
43096
  max_wall_clock_seconds: external_exports.number().int().nullable(),
42918
43097
  cost_budget_usd: external_exports.string().nullable(),
42919
43098
  iterations_used: external_exports.number().int(),
42920
- created_at: external_exports.string()
43099
+ created_at: external_exports.string(),
43100
+ scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
42921
43101
  });
42922
43102
  var softwarePhaseRunSummarySchema = external_exports.object({
42923
43103
  id: uuid10,
@@ -42926,7 +43106,8 @@ var softwarePhaseRunSummarySchema = external_exports.object({
42926
43106
  attempt_no: external_exports.number().int(),
42927
43107
  entered_at: external_exports.string().nullable(),
42928
43108
  exited_at: external_exports.string().nullable(),
42929
- created_at: external_exports.string()
43109
+ created_at: external_exports.string(),
43110
+ scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
42930
43111
  });
42931
43112
  var softwareGateSummarySchema = external_exports.object({
42932
43113
  id: uuid10,
@@ -43077,7 +43258,8 @@ var softwareRequestCreateOutputSchema = external_exports.object({
43077
43258
  software_project_id: uuid10,
43078
43259
  status: softwareRequestStatusSchema,
43079
43260
  current_phase: softwarePhaseSchema,
43080
- phase_run_id: uuid10
43261
+ phase_run_id: uuid10,
43262
+ scheduler_state: softwarePhaseSchedulerStateSchema
43081
43263
  });
43082
43264
  var softwareRequestListInputSchema = external_exports.object({
43083
43265
  limit: external_exports.number().int().positive().max(200).optional()
@@ -43955,6 +44137,23 @@ var softwareVercelDeployPreviewOutputSchema = external_exports.object({
43955
44137
  status: softwareVercelDeploymentStatusSchema
43956
44138
  });
43957
44139
 
44140
+ // ../../packages/protocol/src/software-factory-events.ts
44141
+ var uuid11 = external_exports.string().uuid();
44142
+ var softwareFactoryPhaseRunCompletedEventSchema = external_exports.object({
44143
+ tenantId: uuid11,
44144
+ phaseRunId: uuid11,
44145
+ buildRequestId: uuid11.nullish(),
44146
+ status: external_exports.enum(["passed", "failed"]),
44147
+ idempotencyKey: external_exports.string().min(1).optional()
44148
+ }).strict();
44149
+ var softwareFactoryBudgetExhaustedEventSchema = external_exports.object({
44150
+ tenantId: uuid11,
44151
+ phaseRunId: uuid11,
44152
+ buildRequestId: uuid11,
44153
+ reason: external_exports.string().min(1),
44154
+ idempotencyKey: external_exports.string().min(1).optional()
44155
+ }).strict();
44156
+
43958
44157
  // ../../packages/protocol/src/sync-brief.ts
43959
44158
  var uuidSchema15 = external_exports.string().uuid();
43960
44159
  var dateSchema = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/);
@@ -44198,6 +44397,13 @@ var issueEventPayloadSchema = external_exports.object({
44198
44397
  severity: external_exports.enum(["low", "med", "high", "critical"])
44199
44398
  }).strict();
44200
44399
 
44400
+ // ../../packages/protocol/src/held-tool-action.ts
44401
+ var heldToolActionReplaySchema = external_exports.object({
44402
+ tool_name: external_exports.string().min(1),
44403
+ args: external_exports.record(external_exports.string(), external_exports.unknown()),
44404
+ run_id: external_exports.string().uuid()
44405
+ }).strict();
44406
+
44201
44407
  // ../../packages/protocol/src/operating.ts
44202
44408
  var uuidSchema17 = external_exports.string().uuid();
44203
44409
  var taskListInputSchema = external_exports.object({}).strict();
@@ -44363,7 +44569,12 @@ var escalationDecisionOutputSchema = external_exports.object({
44363
44569
  status: escalationStatusSchema,
44364
44570
  decision_id: uuidSchema18,
44365
44571
  decided_by: uuidSchema18,
44366
- issue_id: uuidSchema18.nullable()
44572
+ issue_id: uuidSchema18.nullable(),
44573
+ // DER-1478: true when the resolved escalation carries a durable replay_action
44574
+ // (an approval-gated connector write) that the approvals path should actuate
44575
+ // after this decision is recorded. Omitted (not false) for non-connector
44576
+ // escalations to keep existing callers' output shape unchanged.
44577
+ replay_action_pending: external_exports.boolean().optional()
44367
44578
  }).strict();
44368
44579
 
44369
44580
  // ../../packages/protocol/src/index.ts
@@ -44789,7 +45000,7 @@ The Compass is drafted, then activated by a human through supersession.
44789
45000
  order: 15,
44790
45001
  title: "AICOS chat guide",
44791
45002
  summary: "How the AI Chief of Staff chat works in the authenticated app shell and what it can safely do today.",
44792
- version: "2026-07-08.1",
45003
+ version: "2026-07-10.2",
44793
45004
  public: true,
44794
45005
  audiences: ["human", "in_app_agent"],
44795
45006
  stages: ["company_setup", "operating_rhythm"],
@@ -44857,10 +45068,12 @@ Each turn carries server-normalized page context: route template, current path,
44857
45068
 
44858
45069
  The current chat harness shows the effective lane and model. Cloud is the default and remains the enforced lane during onboarding, even if a post-onboarding lane has been selected. After onboarding, an owner or admin can select cloud, runner, or MCP only when the server says that lane is ready. Lane changes update the governed AICOS agent seat and write an append-only audit event; the browser never decides tenant authority or runner/MCP readiness.
44859
45070
 
44860
- AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). Runner can target the paired machine's Claude subscription or Codex subscription. MCP is labeled as the MCP client. Read this selection with \`aicos.brain_settings.get\`; owners can update it with \`aicos.brain_settings.update\` (CLI flags: \`--lane\`, \`--cloud-brain managed|byok\`, \`--runner-brain claude-cli|codex-cli\`). BYOK requires an active tenant Anthropic key. Runner and MCP answers are labeled in the transcript and do not consume the managed cloud allowance.
45071
+ AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). Runner brain preference still records Claude or Codex, but interactive AICOS runner turns are currently gated to Claude while the Codex path remains unverified. MCP is labeled as the MCP client. Read this selection with \`aicos.brain_settings.get\`; owners can update it with \`aicos.brain_settings.update\` (CLI flags: \`--lane\`, \`--cloud-brain managed|byok\`, \`--runner-brain claude-cli|codex-cli\`). BYOK requires an active tenant Anthropic key. Choosing \`codex-cli\` for AICOS runner mode is currently rejected with a precondition error until the governed interactive runner path is verified. Runner and MCP answers are labeled in the transcript and do not consume the managed cloud allowance. If the selected runner reports that its active local model account has exhausted its allowance, Settings surfaces **Allowance exhausted** before the user sends another AICOS turn so they can switch lanes, switch runner accounts, or keep working manually instead of waiting on a doomed runner round-trip.
44861
45072
 
44862
45073
  Runner mode uses the same AICOS sessions and transcript. Interactive chat turns are tracked separately from scheduled work orders: each user message has one user-owned turn execution with a short deadline, so two users' chat messages cannot collapse into the same scheduled-work slot. Scheduled and Forge runner work still use work orders. Runner claim packets carry ids and execution metadata, not the user's free-text request. After claim, the local runner calls the governed AICOS context-load tool, which re-checks seat scope and returns the bounded transcript, session summary, route context, tenant clock, grounding text, and source counts authorized for that turn. For execute-ready runners, the claim also carries a server-built Seat work contract that names the governed runtime profile, the AICOS context-loader tool, and the Seat's permission manifest; action attempts still route through command guards, tool-call audit, and pending confirmations. The runner treats loaded tenant clock, context, and contract as authoritative instead of substituting its own runtime clock, local files, or self-invented authority. A runner must be paired, execute-ready, and backed by the live AICOS agent before it can be selected.
44863
45074
 
45075
+ Cloud and runner submissions carry a stable request ID. Distinct requests that compete while a turn is active receive HTTP \`409\` with code \`AICOS_TURN_CONFLICT\`; the losing request writes no partial user message and consumes none of its attachments. Runner acquisition commits its user message, execution, attachment links, queued transcript projection, and session snapshot together, so a failed projection leaves none of them half-written. Retrying the same request ID returns the original turn and does not rerun the model, builder, onboarding, or runner-queue side effects, even if the currently selected lane or runner readiness changed after the original submission.
45076
+
44864
45077
  When a runner claims an interactive AICOS turn, the runner reports start and final result back to {{brand}}. The turn row records claim/start/finish timing, terminal reason, model label, and token counts when the runner or cloud path provides them. The server appends exactly one assistant transcript message for the result, links that message to run evidence, and treats duplicate or late result reports as idempotent. If the runner answer includes a validated in-app link, the web panel can surface the same consent-required navigation suggestion once polling sees that the pending turn is terminal. If a queued, claimed, or running interactive turn passes its short deadline before a runner writes back, the turn is marked offline and the transcript receives a terminal assistant message instead of leaving a permanent "queued" placeholder.
44865
45078
 
44866
45079
  While a runner turn is queued, claimed, or running, the chat panel keeps showing the pending state and polls the session until the answer or terminal status arrives. The composer, model picker, and lane picker are disabled during that active turn so a second browser tab or direct API call cannot overlap another runner turn in the same thread. Terminal runner statuses such as failed, offline, or canceled stay visible only while they are still the latest user turn; if the user continues the conversation in another lane, the stale terminal marker is no longer projected at the bottom of the thread.
@@ -44875,7 +45088,7 @@ You can attach images to a message from the composer. {{brand}} accepts PNG, JPE
44875
45088
 
44876
45089
  AICOS can also retrieve bounded summaries from prior sessions owned by the same tenant user and surface them as session-memory read models. Setup-limited users do not retrieve sensitive historical run or company-memory summaries through this path. This is a practical memory seam for AICOS continuity; it is not yet the future Company Brain, and it does not make unaccepted knowledge authoritative.
44877
45090
 
44878
- Platform allowance and BYOK state are checked before routing; an exhausted included allowance keeps AICOS in guided mode and points the user toward BYOK, billing, or manual continuation. The chat transcript and Settings surface the exhaustion reason and provide direct paths to add a tenant Anthropic key or review the plan. Because the DER-1068 implementation still uses the deterministic harness, it does not create fake llm_calls rows or settle fake allowance usage; reservation/settlement belongs to the real provider call path.
45091
+ Platform allowance and BYOK state are checked before routing; an exhausted included allowance keeps AICOS in guided mode and points the user toward BYOK, billing, or manual continuation. The chat transcript and Settings surface the exhaustion reason and provide direct paths to add a tenant Anthropic key or review the plan. Cloud AICOS provider attempts write canonical run evidence and one linked \`llm_calls\` row with source metadata \`aicos_chat\`, prompt version, latency, token usage, cost, key source, and typed outcome. Managed-key turns consult the tenant hard cap before provider execution; BYOK turns never fall back to the managed platform key. Provider timeouts, provider server errors, empty output, and BYOK credential or allowance failures stay visible as terminal failed turns with retry or Settings recovery instead of being converted into a successful generic answer.
44879
45092
 
44880
45093
  During onboarding, the deterministic harness may execute only explicit low-risk draft actions. In the Responsibility Graph step, a user with setup/action authority can ask AICOS to create one named draft seat per turn; the seat stays vacant, is traceable to the AICOS message through artifact sources, and still needs human review before staffing, Charters, manifests, or go-live. Compass and Charter help remains draft guidance and clarifying questions until the live drafting planner lands. AICOS never silently finishes onboarding, approves Compass or Charter content, signs manifests, goes live, stores credentials, changes autonomy policy, or invites external people; those actions remain human-confirmed.
44881
45094
 
@@ -44938,7 +45151,7 @@ Treat AICOS as a coordinator over the operating system. It can become more usefu
44938
45151
  order: 20,
44939
45152
  title: "Responsibility Graph playbook",
44940
45153
  summary: "How to build a functions-first graph with seats, owners, Stewards, vacancies, and clean authority.",
44941
- version: "2026-07-07.2",
45154
+ version: "2026-07-10.1",
44942
45155
  public: true,
44943
45156
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44944
45157
  stages: ["graph_design", "staffing"],
@@ -44982,7 +45195,7 @@ The graph always resolves to a single canonical root seat (Founder & CEO in the
44982
45195
 
44983
45196
  AICOS always stays a peer of the root (never a child of its steward) and never appears as an orphan in the unassigned set \u2014 this is intentional and not a data-quality issue.
44984
45197
 
44985
- Seats that do not reach the single root through the resolved parent chain are listed as **unassigned** (with \`unassigned: true\` and no incoming edge). Each \`graph.get\` node carries an \`unassigned\` boolean, and the CLI \`rost graph show\` output prints unassigned seats under a separate \`Unassigned\` section so operators can see which parts of the chart need attention. The web canvas parks unassigned seats at the bottom of the chart instead of inventing fake root edges.
45198
+ Seats that do not reach the single root through the resolved parent chain are listed as **unassigned** (with \`unassigned: true\` and no incoming edge). Each \`graph.get\` node carries an \`unassigned\` boolean, and the CLI \`rost graph show\` output prints unassigned seats under a separate \`Unassigned\` section so operators can see which parts of the chart need attention. The web canvas parks unassigned seats at the bottom of the chart instead of inventing fake root edges, and when any exist a floating **Unassigned seats \u2014 needs attention** panel lists them by name; selecting one flies the canvas to that seat so the gap is one click from a fix, not just visible in the baseline row.
44986
45199
 
44987
45200
  ## Forge Developer Team seats are pinned
44988
45201
 
@@ -45015,7 +45228,9 @@ The graph also carries a mission-control panel. It pulls the same operational st
45015
45228
 
45016
45229
  ## An agent seat's profile
45017
45230
 
45018
- A staffed agent's seat page opens on a profile built to answer one question in ten seconds: what is this agent, what does it know, what can it touch, and who is accountable. The hero shows live/paused/draft status, a one-line description, and when it last ran; a right-hand fact sheet stays visible on every tab with the Charter version, Skills, Connections (with a "connect" prompt when a Skill needs a provider that is not yet linked), an Access summary, and tool/held/model counts. Four tabs organize the rest: **Overview** (recent runs as one-line summaries, 30-day cost by model, and a version-history feed that tracks the agent itself as an actor alongside human Charter decisions), **Charter** (the accepted Charter rendered as a document \u2014 purpose, responsibilities, owned measurables, decision authority \u2014 with its version chip, signer, and an Amend action that opens the same draft-and-confirm amendment flow as the Charter Builder), **Access** (plain-language cards for what the agent does while you are watching versus running unattended, then every granted tool grouped under its Connection with a one-line description and Read/Draft/Held status), and **Activity** (the full Trust Card below). AICOS's seat keeps its own operations panel instead of this profile, because it is the company's foundation seat rather than a generic staffed agent.
45231
+ A staffed non-AICOS agent's seat page uses one profile shell with eight URL-addressable tabs: **Overview**, **Charter**, **Tools & connections**, **Skills**, **Autonomy**, **Signals**, **Activity**, and **Settings**. The hero shows status, Charter purpose, Steward, seat path, lane, model, and last run. The sidebar keeps access, connection, tool/Skill/Signal/escalation/Charter counters, and schedule facts visible while the tabs organize the existing read models and controls. Legacy links such as \`#agent-ops\`, \`#recent-runs\`, and \`#run-<id>\` select the matching Settings or Activity tab. Human seats and the AICOS foundation seat keep their dedicated pages. Members without sensitive-data access see the same shell with explicit unavailable states for run, tool-call, cost, connection-status, and escalation facts; the page never broadens Trust Card access or turns an unavailable read into a confident empty/disconnected claim.
45232
+
45233
+ Agent creation at \`/agents/new\` begins with four doors: **Describe the job** opens the existing in-app builder, **Start from a template** opens the stock-template picker, **Build manually** opens the custom setup path, and **Import** opens an in-app format explainer. Definition-file import is roadmap-only and never opens a bare file picker; operators can instead continue to the existing-agent pairing flow. Existing \`?mode=template|custom|existing\` and \`?seat=\` resume links continue directly to their current flows.
45019
45234
 
45020
45235
  ## Review an agent seat's delivered work
45021
45236
 
@@ -45334,7 +45549,7 @@ For CLI and MCP setup, read agent-skill-setup-guide before adding or assigning S
45334
45549
  order: 42,
45335
45550
  title: "Design a custom agent",
45336
45551
  summary: "How to build a custom agent from operational questions through the Charter Builder, tools, dry run, and go-live without writing prompts.",
45337
- version: "2026-07-04.1",
45552
+ version: "2026-07-10.1",
45338
45553
  public: true,
45339
45554
  audiences: ["human", "cli", "mcp", "in_app_agent"],
45340
45555
  stages: ["staffing", "charter_design"],
@@ -45380,7 +45595,7 @@ Then choose one of three named triggers:
45380
45595
  - **Scheduled** \u2014 runs on a recurring cadence you pick (every weekday morning, every morning, weekly, hourly). No cron to write.
45381
45596
  - **Event** \u2014 runs in response to work routed to it, like a sync or a mention, rather than on a clock.
45382
45597
 
45383
- Open **Advanced** for the explicit lane select and a raw cron expression when you need a custom cadence. The same schedule presets appear on the agent's seat page after go-live (Agent operations \u2192 Run schedule).
45598
+ Open **Advanced** for the explicit lane select and a raw cron expression when you need a custom cadence. A raw schedule is a five-field UTC cron \u2014 \`minute hour day-of-month month day-of-week\` \u2014 supporting \`*\`, \`*/N\` steps, \`A-B\` ranges, and comma lists (for example \`0 9 * * 1-5\`). Day-of-week is 0\u20137 where both 0 and 7 mean Sunday. It is validated when you save: an unsupported or out-of-range expression is rejected with a field-level error rather than saved as a schedule that never runs, and the builder previews the next run in UTC. When both day-of-month and day-of-week are set, a run fires only when both match. The same schedule presets appear on the agent's seat page after go-live (Agent operations \u2192 Run schedule).
45384
45599
 
45385
45600
  ## Configure tools and credentials
45386
45601
 
@@ -45598,7 +45813,7 @@ Use the lower-level commands after health names a finding: \`agent.get_run\` for
45598
45813
  order: 46,
45599
45814
  title: "Tool access and vault",
45600
45815
  summary: "How to give agents access to tools without exposing raw credentials or expanding authority by accident.",
45601
- version: "2026-07-02.3",
45816
+ version: "2026-07-02.4",
45602
45817
  public: true,
45603
45818
  audiences: ["human", "cli", "mcp", "in_app_agent"],
45604
45819
  stages: ["staffing"],
@@ -45656,7 +45871,7 @@ Google can be connected from Settings once the workspace OAuth app is configured
45656
45871
 
45657
45872
  CLI and MCP can inspect connector readiness without seeing secrets: \`integration.list\` / \`rost_list_integrations\` lists connected providers and health metadata, \`integration.status\` / \`rost_get_integration_status\` reads one provider by id or name, and \`integration.test\` / \`rost_test_integration_connection\` runs the installed provider-specific health check. For Google, the test refreshes the vaulted OAuth credential and reads the Gmail profile, then records only account, scope, and health metadata.
45658
45873
 
45659
- \`integration.connect_rest\` / \`rost_connect_rest_integration\` creates or rotates the supported Baserow REST integration through the credential flow. It stores the token in the vault, persists only endpoint/account metadata plus a tenant-scoped integration row, marks the adapter as \`rest\`, and returns no vault ref or secret. Because the input includes a raw secret, generated CLI argv refuses the command; use Settings or the command API credential flow instead of shell arguments.
45874
+ \`integration.connect_rest\` creates or rotates the supported Baserow REST integration through the credential flow. It stores the token in the vault, persists only endpoint/account metadata plus a tenant-scoped integration row, marks the adapter as \`rest\`, and returns no vault ref or secret. Because the input includes a raw secret, generated CLI argv refuses the command and it is deliberately not exposed as an agent-callable MCP tool; run it from Settings or the command API credential flow instead of shell arguments.
45660
45875
 
45661
45876
  \`integration.readiness\` / \`{{cli}} integration readiness --provider google --json\` / \`rost_check_integration_readiness\` returns the operator setup checklist: app configuration status, tenant connection state, granted scopes, latest test state, the demo-safe partner path, external verification/CASA caveats, handler availability, and the next operator action. The checklist is metadata-only. It never returns access tokens, refresh tokens, client secrets, vault refs, or raw provider responses. Settings deliberately keeps the live Google card narrower: connected account, last successful test, granted capabilities, and connect/reconnect/test actions.
45662
45877
 
@@ -45676,7 +45891,7 @@ There is exactly one way to give a connected tool its credential, and it is the
45676
45891
 
45677
45892
  - Stage tools on a draft agent: \`agent.configure_tools\` / \`rost_configure_agent_tools\` \u2014 connect or decline proposed tools and stage credential-ingress requests. Pass vault references, never raw secret material.
45678
45893
  - Inspect connector readiness: \`integration.list\` / \`rost_list_integrations\`, \`integration.status\` / \`rost_get_integration_status\`, and \`integration.test\` / \`rost_test_integration_connection\` return provider metadata and health only.
45679
- - Store a secret: \`credential.ingress\` / \`rost_store_credential\` (scope: seat) persists only a vault reference. \`credential.ingress\` is \`credential_flow\` \u2014 it returns a pending confirmation and runs only with a real human-provided secret, captured as a vault reference. (Because it redacts that secret, the pending confirmation also shows a high-risk badge \u2014 see the confirmations guide for badge-versus-level.)
45894
+ - Store a secret: \`credential.ingress\` (scope: seat) persists only a vault reference. It is \`credential_flow\` \u2014 because it carries a real human-provided secret it runs only through the Settings vault-ingress flow (a human session); generated CLI argv refuses it and it is deliberately not an agent-callable MCP tool. (Because it redacts that secret, its confirmation also shows a high-risk badge \u2014 see the confirmations guide for badge-versus-level.)
45680
45895
  - Sign the manifest: \`charter.sign_manifest\` / \`rost_sign_charter_manifest\` requests human confirmation for the seat's permission manifest.
45681
45896
  - Mint local access: prefer \`{{cli}} mcp install\` for users; \`mcp_token.create\` is \`human_required\` and returns the token once. List metadata with \`mcp_token.list\` (never token material); revoke with \`mcp_token.revoke\`.
45682
45897
 
@@ -45744,7 +45959,7 @@ External connectors are being rolled out provider by provider, conservatively (r
45744
45959
  order: 48,
45745
45960
  title: "CLI and MCP installation guide",
45746
45961
  summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
45747
- version: "2026-07-05.7",
45962
+ version: "2026-07-10.3",
45748
45963
  public: true,
45749
45964
  audiences: ["human", "cli", "mcp", "in_app_agent"],
45750
45965
  stages: ["company_setup", "staffing"],
@@ -46105,7 +46320,7 @@ A leaked tenant-admin token can administer the whole tenant, not just one seat.
46105
46320
 
46106
46321
  ### Storing the Anthropic key and other credentials
46107
46322
 
46108
- Storing the tenant model key or any other secret goes through **credential ingress** as a vault reference \u2014 the secret is never pasted into a prompt, config, or log. Use \`rost_save_tenant_anthropic_key\` (\`tenant.anthropic_key.save\`) for the tenant Anthropic key, or \`rost_store_credential\` (\`credential.ingress\`) for other secrets; \`rost_configure_agent_tools\` stages credential requests but never accepts raw secrets. Credential storage is a gated \`credential_flow\` confirmation that a human approves. For the vault model and the security posture behind this, see the security-model-guide and the tool-access-and-vault guide; for model-bound data, BYOK provider handling, and local-client provider settings, see the ai-model-data-handling-guide; for the confirmation gate, see the confirmations-guide.
46323
+ Storing the tenant model key or any other secret goes through **credential ingress** as a vault reference \u2014 the secret is never pasted into a prompt, config, or log. Use \`rost_save_tenant_anthropic_key\` (\`tenant.anthropic_key.save\`) for the tenant Anthropic key; other secrets go through the Settings vault-ingress flow (\`credential.ingress\`), a human-only interactive flow that is deliberately not an agent-callable MCP tool and refuses raw secrets over CLI argv. \`rost_configure_agent_tools\` stages credential *requests* (provider, scope, and label only) but never accepts raw secrets. Credential storage is a gated \`credential_flow\` confirmation that a human approves. For the vault model and the security posture behind this, see the security-model-guide and the tool-access-and-vault guide; for model-bound data, BYOK provider handling, and local-client provider settings, see the ai-model-data-handling-guide; for the confirmation gate, see the confirmations-guide.
46109
46324
 
46110
46325
  ## When to use MCP or CLI
46111
46326
 
@@ -46210,7 +46425,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
46210
46425
  | \`{{cli}} integration list|readiness|status|test\` / credential flow for Baserow REST connect | \`integration.connect_rest\`, \`integration.list\`, \`integration.readiness\`, \`integration.status\`, \`integration.test\` | Create a vault-backed Baserow REST integration through the credential flow, list connector metadata, read setup-readiness, read one connector's health, and run provider-specific tests without exposing credentials. | Tenant-admin for connect; tenant for reads/tests | \`{{cli}} integration readiness --provider google --json\` |
46211
46426
  | \`{{cli}} system health\` | \`system.health\` | Read the scoped system-health snapshot across agent runs, unresolved errors, Signals, Cascade setup, work loop, integrations, runners, notifications, and Sync Brief readiness. | Tenant, member, or seat token | \`{{cli}} system health --json\`; \`{{cli}} system health --seat <id> --json\` |
46212
46427
  | \`{{cli}} command usage.report|usage.snapshot\` | \`usage.report\`, \`usage.snapshot\` | Read the AI-workforce ROI report (per-period agent turns, token/run cost, human-accepted value, exceptions, approvals) from immutable usage snapshots, and freeze a period's totals into a new snapshot (insert-only, idempotent). | Tenant | \`{{cli}} command usage.report --json\`; \`{{cli}} command usage.snapshot --json\` |
46213
- | \`{{cli}} settings get|update|product-learning|agent-policy|rename\` | \`settings.get\`, \`settings.update\`, \`settings.product_learning.get\`, \`settings.product_learning.update\`, \`settings.agent_policy.get\`, \`settings.agent_policy.update\`, \`tenant.rename\` | Read tenant settings, update budget caps, read or update product-learning participation, read or set the company autonomy ceiling, and rename the company. Budget, product-learning, and policy updates stop at human confirmation in non-interactive CLI/MCP sessions; company rename is owner-only and human-gated. | Tenant / Tenant-admin for updates | \`{{cli}} settings product-learning get --json\`; \`{{cli}} settings agent-policy get --json\`; \`{{cli}} settings rename --company "Acme Operations"\` |
46428
+ | \`{{cli}} settings get|update|product-learning|agent-policy|confirmation-mode|rename\` | \`settings.get\`, \`settings.update\`, \`settings.product_learning.get\`, \`settings.product_learning.update\`, \`settings.agent_policy.get\`, \`settings.agent_policy.update\`, \`settings.confirmation_policy.get\`, \`settings.confirmation_policy.update\`, \`tenant.rename\` | Read tenant settings, update budget caps, read or update product-learning participation, read or set the company autonomy ceiling, read or set the CLI confirmation mode, and rename the company. Budget, product-learning, and policy updates stop at human confirmation in non-interactive CLI/MCP sessions; company rename is owner-only and human-gated. | Tenant / Tenant-admin for updates | \`{{cli}} settings product-learning get --json\`; \`{{cli}} settings agent-policy get --json\`; \`{{cli}} settings confirmation-mode get --json\`; \`{{cli}} settings rename --company "Acme Operations"\` |
46214
46429
  | \`{{cli}} member invite|update|remove\` | \`member.invite\`, \`member.update\`, \`member.remove\` | Manage tenant members; execution requires an owner or admin membership. | Tenant | \`{{cli}} member invite --email ops@example.com --role member\` |
46215
46430
  | \`{{cli}} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show\` | \`agent_template.list\`, \`agent.create_from_template\`, \`agent.create_custom\`, \`agent_setup.get\`, \`agent_setup.update\`, \`agent.configure_tools\`, \`agent.run_dry_run\`, \`agent.go_live\`, \`agent.status\`, \`agent.run_now\`, \`agent.fleet_digest\`, \`agent.get_run\`, \`agent.show_markdown\` | Run the full agent setup and operation flow: list templates, create a draft from a template or guided custom answers (with \`--model\` and \`--effort\`), read or answer setup state, connect or decline tools, dry-run, go live, run on demand, capture the fleet-health digest, read one run's transcript/error diagnostics, and show a markdown readout. Human CLI dry-runs print the rehearsal transcript and, when present, the per-tool preview labels (\`Would run\`, \`Blocked\`, \`Escalated\`) before go-live. Create and go-live stop at human gates; the dry-run is ungated by human approval but requires a signed manifest first. | Tenant and seat | \`{{cli}} agent fleet-digest --json\` |
46216
46431
  | \`{{cli}} tools list\` | \`tool.catalog\` | List the discoverable tool catalog the builder reads (id, scope tiers, credential requirement, access policy, and execution-boundary guidance). | Tenant | \`{{cli}} tools list --json\` |
@@ -46223,7 +46438,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
46223
46438
  | \`{{cli}} deliverable list|get|create|attach\` | \`deliverable.list\`, \`deliverable.get\`, \`deliverable.create\`, \`deliverable.attach\` | List, get, create, or attach agent deliverables for a seat. Deliverables are durable work outputs visible across UI, CLI, and MCP. | Tenant (list/get), Seat (create/attach) | \`{{cli}} deliverable list --seat-id <id> --json\`; \`{{cli}} deliverable create --title "Brief" --kind brief\` |
46224
46439
  | \`{{cli}} graph show\` | \`graph.get\` | Print a table view of the Responsibility Graph with explicit \`seat_id\` and \`parent_seat_id\` columns. | Tenant | \`{{cli}} graph show\` |
46225
46440
  | \`{{cli}} seat list|get|create|rename|reparent|decommission\` | \`graph.get\`, \`seat.get\`, \`seat.create\`, \`seat.rename\`, \`seat.reparent\`, \`seat.decommission\`, \`seat.decommission_preview\` | Work with seats as first-class CLI primitives. \`seat decommission --dry-run\` previews affected occupancies, agents, Charters, tokens, credentials, work orders, and schedules before the human-gated teardown. | Tenant / Seat for targeted mutations | \`{{cli}} seat list\`; \`{{cli}} seat decommission --seat-id <id> --dry-run\` |
46226
- | \`{{cli}} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume\` | \`software_factory.project.create\`, \`software_factory.project.list\`, \`software_factory.request.create\`, \`software_factory.request.list\`, \`software_factory.request.show\`, \`software_factory.request.pause\`, \`software_factory.request.cancel\`, \`software_factory.request.resume\` | Create/list Forge projects, open/read governed Forge build requests, and control request lifecycle. Project creation plus pause/cancel/resume are tenant-admin, entitlement-gated, and human-gated; reads and request creation require the Forge add-on. Pause halts new runner claims at the next task checkpoint, cancel drives terminal canceled, and resume requeues from the last completed task checkpoint. | Tenant / Tenant-admin for create and lifecycle controls | \`{{cli}} forge project-create --name "Leiluna app" --base-branch main\`; \`{{cli}} forge request-create --project <slug-or-name> --title "Add invoice export"\` (accepts \`--software-project-id <id>\` instead of \`--project\` too); \`{{cli}} forge request-pause --build-request-id <id> --reason "Operator hold"\` |
46441
+ | \`{{cli}} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume\` | \`software_factory.project.create\`, \`software_factory.project.list\`, \`software_factory.request.create\`, \`software_factory.request.list\`, \`software_factory.request.show\`, \`software_factory.request.pause\`, \`software_factory.request.cancel\`, \`software_factory.request.resume\` | Create/list Forge projects, open/read governed Forge build requests, and control request lifecycle. Request create/list/show return \`scheduler_state\`: \`queued\` when a runner work order exists, \`parked\` when the Forge team is staffed but no paired online runner can claim it, and \`not_configured\` when the runner-lane Forge Seat is missing. Parked and not-configured states link to \`/settings/runners/setup\`; runner pairing or the repair sweep re-enqueues the same pending intake idempotently. Project creation plus pause/cancel/resume are tenant-admin, entitlement-gated, and human-gated; reads and request creation require the Forge add-on. | Tenant / Tenant-admin for create and lifecycle controls | \`{{cli}} forge project-create --name "Leiluna app" --base-branch main\`; \`{{cli}} forge request-create --project <slug-or-name> --title "Add invoice export"\` (accepts \`--software-project-id <id>\` instead of \`--project\` too); \`{{cli}} forge request-pause --build-request-id <id> --reason "Operator hold"\` |
46227
46442
 
46228
46443
  Skills wrapper help:
46229
46444
 
@@ -46316,8 +46531,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46316
46531
  | \`rost_skip_charter_draft\` | \`charter.skip\` | Mark a Charter draft as skipped. | Seat or tenant-admin | Call only when a human defers the seat. |
46317
46532
  | \`rost_apply_charter_seat_type_recommendation\` | \`charter.apply_seat_type_recommendation\` | Apply a Charter's seat-type recommendation. | Seat or tenant-admin | Call after reviewing the draft. |
46318
46533
  | \`rost_sign_charter_manifest\` | \`charter.sign_manifest\` | Request human confirmation for a permission manifest. | Seat or tenant-admin | Use after tool permissions are reviewed. |
46319
- | \`rost_approve_pending_confirmation\` | \`confirmation.approve\` | Approve a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
46320
- | \`rost_reject_pending_confirmation\` | \`confirmation.reject\` | Reject a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
46321
46534
  | \`rost_draft_compass\` | \`compass.draft\` | Create a Compass draft. | Tenant | Call with a concise Compass document. |
46322
46535
  | \`rost_update_compass_draft\` | \`compass.update_draft\` | Replace a Compass draft document. | Tenant | Call with the \`compass_version_id\` from \`compass.draft\` as \`draft_id\` and the updated document. |
46323
46536
  | \`rost_approve_compass_version\` | \`compass.approve_version\` | Activate a Compass version by supersession. | Tenant | Call after human review. |
@@ -46327,7 +46540,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46327
46540
  | \`rost_assign_user_to_seat\` | \`staffing.assign_user\` | Assign a human user occupancy to a seat. | Seat or tenant-admin | Call with \`seat_id\` and \`user_id\`. |
46328
46541
  | \`rost_assign_dry_run_agent_to_seat\` | \`staffing.assign_agent_dry_run\` | Assign an agent occupancy in dry-run mode. | Seat or tenant-admin | Call only after Steward chain is clear. |
46329
46542
  | \`rost_staff_seat\` | \`staffing.assign\` | Compatibility helper for human, agent, or hybrid staffing. | Seat or tenant-admin | Call with \`seat_id\` and \`occupant\`. |
46330
- | \`rost_store_credential\` | \`credential.ingress\` | Store a secret through the vault and persist only a vault reference. | Seat or tenant-admin | Use vault-backed values only. |
46331
46543
  | \`rost_go_live\` | \`agent.go_live\` | Promote a dry-run agent seat to live. | Seat or tenant-admin | Call after human approval. |
46332
46544
  | \`rost_create_mcp_token\` | \`mcp_token.create\` | Mint a tenant-admin or seat-scoped MCP token. | Tenant | Prefer \`{{cli}} mcp install\` for users. |
46333
46545
  | \`rost_revoke_mcp_token\` | \`mcp_token.revoke\` | Revoke an MCP token immediately. | Tenant | Call with \`token_id\`. |
@@ -46354,7 +46566,7 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46354
46566
  | \`rost_create_agent_from_template\` | \`agent.create_from_template\` | Create a draft stock agent and draft Charter from a template (draft-only; occupancy needs a human steward). | Tenant | Call with \`seat_id\` and \`template_slug\`; expect human confirmation. |
46355
46567
  | \`rost_start_agent_setup\` | \`agent_setup.start\` | Start an agent setup draft for template, custom, or existing mode. | Tenant | Call with \`mode\` and seat placement. |
46356
46568
  | \`rost_get_agent_setup\` | \`agent_setup.get\` | Read agent setup state, blockers, and next action. | Seat or tenant-admin | Call with \`{"setup_id":"<id>"}\`. |
46357
- | \`rost_update_agent_setup\` | \`agent_setup.update\` | Update parent, steward, lane, schedule, or answers on a setup draft. | Tenant | Call with \`setup_id\` and the fields to change. |
46569
+ | \`rost_update_agent_setup\` | \`agent_setup.update\` | Update parent, steward, lane, schedule, answers, or skill discovery on a setup draft. | Tenant | Call with \`setup_id\` and the fields to change. |
46358
46570
  | \`rost_update_agent_schedule\` | \`agent.update_schedule\` | Update a draft or live agent's scheduled execution. | Tenant | Call with \`agent_id\` and \`schedule_cron\`; expect human confirmation. |
46359
46571
  | \`rost_decommission_agent\` | \`agent.decommission\` | Retire an agent occupancy safely (no-orphan guarded). | Tenant | Call with \`agent_id\`; expect human confirmation. |
46360
46572
  | \`rost_pause_agent\` | \`agent.pause\` | Pause a live agent so it stops scheduled and on-demand runs; reversible. | Tenant | Call with \`seat_id\`; expect human confirmation. |
@@ -46381,7 +46593,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46381
46593
  | \`rost_resolve_error_log\` | \`error_log.resolve\` | Acknowledge or resolve an error log with a required human reason and optional task, issue, or PR link. | Tenant | Call with \`error_log_id\`, \`reason\`, and \`disposition\` \`acknowledged\` or \`resolved\`. |
46382
46594
  | \`rost_supersede_error_log\` | \`error_log.supersede\` | Mark an older error log as superseded by a newer in-tenant error log. | Tenant | Call with \`error_log_id\`, \`new_error_log_id\`, and \`reason\`. |
46383
46595
  | \`rost_list_integrations\` | \`integration.list\` | List connected integration metadata and latest health state. | Tenant | Call with \`{}\` or \`{"provider":"google"}\`; no secrets or vault refs are returned. |
46384
- | \`rost_connect_rest_integration\` | \`integration.connect_rest\` | Create or rotate the vault-backed Baserow REST integration for Signal pulls. | Tenant-admin | Use the credential flow with provider \`baserow\`, HTTPS endpoint, scope, secret name, and secret; no vault refs or secrets are returned. |
46385
46596
  | \`rost_check_integration_readiness\` | \`integration.readiness\` | Return the connector setup checklist: OAuth env/callback, tenant connection, scopes, latest test state, external verification/CASA, handler availability, and DER-coded next actions. | Tenant | Call with \`{"provider":"google"}\`; metadata only, no secrets or vault refs. |
46386
46597
  | \`rost_get_integration_status\` | \`integration.status\` | Read one integration's metadata by provider or integration id. | Tenant | Call with \`{"provider":"google"}\` or \`{"integration_id":"<id>"}\`. |
46387
46598
  | \`rost_test_integration_connection\` | \`integration.test\` | Run the provider-specific connection test and update integration health. | Tenant | Call with \`{"provider":"google"}\`; result is bounded metadata only. |
@@ -46397,8 +46608,10 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46397
46608
  | \`rost_update_product_learning_policy\` | \`settings.product_learning.update\` | Set product-learning mode. | Tenant-admin | Human-gated; call with \`{"mode":"enabled"}\`, \`{"mode":"disabled"}\`, or \`{"mode":"enterprise_contract"}\`. |
46398
46609
  | \`rost_get_company_autonomy_ceiling\` | \`settings.agent_policy.get\` | Read the company autonomy ceiling (Company Guardrails): profile, enforcement, and max_autonomous_risk. | Tenant | Call with \`{}\`; metadata only. |
46399
46610
  | \`rost_set_company_autonomy_ceiling\` | \`settings.agent_policy.update\` | Set the company autonomy ceiling. | Tenant-admin | Owner-only, human-gated; call with \`{"profile":"locked_down"}\` (or \`balanced\`/\`high_autonomy\`, or \`{"profile":"custom","max_autonomous_risk":"high"}\`). Enforced by default. |
46611
+ | \`rost_get_cli_confirmation_mode\` | \`settings.confirmation_policy.get\` | Read the CLI confirmation mode: \`careful\` (the strict default \u2014 the CLI cannot silently auto-approve a pending confirmation) or \`trusted_operator\` (an owner opt-in). | Tenant | Call with \`{}\`; metadata only. |
46612
+ | \`rost_set_cli_confirmation_mode\` | \`settings.confirmation_policy.update\` | Set the CLI confirmation mode. | Tenant-admin | Owner-only, human-gated; call with \`{"cli_mode":"trusted_operator"}\` (or \`{"cli_mode":"careful"}\`). Choosing \`trusted_operator\` establishes an ADR-0018 \xA76 standing authorization; a \`dangerous\`-risk confirmation always requires interactive human review in either mode. |
46400
46613
  | \`rost_get_aicos_brain_settings\` | \`aicos.brain_settings.get\` | Read AICOS lane and brain labels for cloud managed/BYOK, runner Claude/Codex, and MCP client. | Tenant | Call with \`{}\`; returns enum labels only, never keys, vault refs, or runner secrets. |
46401
- | \`rost_update_aicos_brain_settings\` | \`aicos.brain_settings.update\` | Update AICOS lane and cloud/runner brain preferences. | Tenant-admin | Owner-only and human-gated; call with \`{"lane":"runner","runner_brain":"codex-cli"}\` or \`{"cloud_brain":"byok"}\`. BYOK requires an active tenant Anthropic key. |
46614
+ | \`rost_update_aicos_brain_settings\` | \`aicos.brain_settings.update\` | Update AICOS lane and cloud/runner brain preferences. | Tenant-admin | Owner-only and human-gated; call with \`{"lane":"runner","runner_brain":"claude-cli"}\` or \`{"cloud_brain":"byok"}\`. BYOK requires an active tenant Anthropic key. For AICOS, selecting \`codex-cli\` is currently rejected until the governed interactive runner path is verified. |
46402
46615
  | \`rost_list_signals\` | \`signal.list\` | List measurables with their latest reading and on/off-track state. | Seat or tenant-admin | Call with \`{}\` to find measurable ids. |
46403
46616
  | \`rost_get_signal\` | \`signal.get\` | Read a measurable with its full reading history. | Seat or tenant-admin | Call with \`{"measurable_id":"<id>"}\`. |
46404
46617
  | \`rost_confirm_signal_reading\` | \`signal.confirm_reading\` | Confirm an unconfirmed reading as human-verified. | Seat or tenant-admin | Humans confirm; call with \`{"reading_id":"<id>"}\`. |
@@ -46465,13 +46678,13 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46465
46678
  | \`rost_revoke_skill_from_seat\` | \`skill.revoke_from_seat\` | Human-gated revocation that stops future Skill use without deleting historical activations. | Tenant-admin | Call with \`{"assignment_id":"<assignment-id>"}\`; non-interactive callers receive a confirmation handoff. |
46466
46679
  | \`rost_list_model_catalog\` | \`model.catalog\` | List guided model tiers \u2014 recommendations, token prices, cost bands, best-fit work, and model ids for \`--model\`. | Tenant | Call with \`{}\`. |
46467
46680
  | \`rost_create_a_forge_project\` | \`software_factory.project.create\` | Create an active Forge software project before binding repositories or opening build requests. Tenant-admin, human-gated, and entitlement-gated. | Tenant-admin | Call with \`{"name":"Leiluna app","slug":"leiluna-app","base_branch":"main"}\`; non-interactive callers receive a confirmation handoff. |
46468
- | \`rost_create_a_forge_build_request\` | \`software_factory.request.create\` | Open a governed Forge build request against a connected software project; records the request plus its initial intake phase run. Requires the Forge add-on; the title is untrusted display text. | Tenant | Call with \`{"project":"<slug-or-name>","title":"Add invoice export"}\` (or \`{"software_project_id":"<project-id>", ...}\`). |
46681
+ | \`rost_create_a_forge_build_request\` | \`software_factory.request.create\` | Open a governed Forge build request against a connected software project; records the request plus its initial intake phase run and returns \`scheduler_state\` so MCP callers can distinguish queued work from \`parked\` runner setup or \`not_configured\` staffing gaps. Requires the Forge add-on; the title is untrusted display text. | Tenant | Call with \`{"project":"<slug-or-name>","title":"Add invoice export"}\` (or \`{"software_project_id":"<project-id>", ...}\`). |
46469
46682
  | \`rost_pause_a_forge_build_request\` | \`software_factory.request.pause\` | Pause an in-flight Forge build request. New runner claims stop immediately; running work halts at the next task checkpoint. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Operator hold"}\`; non-interactive callers receive a confirmation handoff. |
46470
46683
  | \`rost_cancel_a_forge_build_request\` | \`software_factory.request.cancel\` | Cancel a Forge build request and expire active runner work orders. This is terminal; use pause for reversible holds. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Out of scope"}\`; non-interactive callers receive a confirmation handoff. |
46471
46684
  | \`rost_resume_a_forge_build_request\` | \`software_factory.request.resume\` | Resume a paused or human-blocked Forge build request from the last completed task checkpoint and requeue the current phase. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Budget raised"}\`; non-interactive callers receive a confirmation handoff. |
46472
46685
  | \`rost_list_forge_projects\` | \`software_factory.project.list\` | List active Forge software projects so a tenant can choose a project for build requests, GitHub repository bindings, and config. | Tenant | Call with \`{}\`; use the returned \`id\` as \`software_project_id\`. |
46473
- | \`rost_list_forge_build_requests\` | \`software_factory.request.list\` | The Forge control-room board: build requests with status, current phase, and risk. Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"limit":20}\`. |
46474
- | \`rost_show_a_forge_build_request\` | \`software_factory.request.show\` | The Forge request detail: the request, current phase, phase-run history, and gates. Read-only; requires the Forge add-on. | Tenant | Call with \`{"build_request_id":"<request-id>"}\`. |
46686
+ | \`rost_list_forge_build_requests\` | \`software_factory.request.list\` | The Forge control-room board: build requests with status, current phase, risk, and current \`scheduler_state\` remediation. Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"limit":20}\`. |
46687
+ | \`rost_show_a_forge_build_request\` | \`software_factory.request.show\` | The Forge request detail: the request, current phase, phase-run history, gates, and current \`scheduler_state\` remediation. Read-only; requires the Forge add-on. | Tenant | Call with \`{"build_request_id":"<request-id>"}\`. |
46475
46688
  | \`rost_advance_a_forge_build_request_phase\` | \`software_factory.phase.advance\` | Advance a build request to its next phase; the server enforces the closed phase state machine and rejects an illegal transition. | Tenant | Call with \`{"build_request_id":"<request-id>","to_phase":"discovery_scoping"}\`. |
46476
46689
  | \`rost_decide_a_forge_gate\` | \`software_factory.gate.decide\` | A human approves or rejects a Forge gate; an agent caller routes to \`/approvals\` and can never self-approve. Sensitive gates record a linked human decision. | Tenant | Call with \`{"gate_id":"<gate-id>","decision":"approve"}\`; non-interactive callers receive a confirmation handoff. |
46477
46690
  | \`rost_answer_forge_plan_clarifications\` | \`software_factory.request.answer_clarification\` | Write structured human answers to a draft plan's clarifying questions and optionally mark it ready for plan review; answers append to the versioned planning artifact and never widen authority or automation mode. | Tenant | Call with \`{"build_request_id":"<request-id>","answers":[{"question":"Which database?","answer":"Postgres"}],"mark_ready_for_plan_review":true}\`. |
@@ -46495,7 +46708,7 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46495
46708
  | \`rost_create_a_forge_vercel_preview_deployment\` | \`software_factory.vercel.deploy_preview\` | Create a Vercel preview deployment for a linked project after Forge deploy-preview authority, the Vercel OAuth vault-ref connection, and required preview secret/config grants are verified. | Tenant | Call with \`{"software_project_id":"<project-id>","commit_sha":"<sha>","git_ref":"feature/example","seat_id":"<seat-id>"}\`; returns deployment metadata, not tokens. |
46496
46709
  | \`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>"}\`. |
46497
46710
 
46498
- The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, request pause/cancel/resume controls, 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, rejecting it, pausing, canceling, or resuming calls the same Forge commands exposed over CLI/MCP. Lifecycle controls deliberately stage pending confirmations before changing runner state. The install button also 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 7 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
46711
+ The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, request pause/cancel/resume controls, runner capacity, configuration status, and Developer Team install. Request cards and detail pages surface \`scheduler_state\` directly: queued work shows its work-order state, parked work explains that no paired online runner is available, and not-configured work sends the operator to \`/settings/runners/setup\`. A request detail page also 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, rejecting it, pausing, canceling, or resuming calls the same Forge commands exposed over CLI/MCP. Lifecycle controls deliberately stage pending confirmations before changing runner state. The install button also 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 7 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
46499
46712
 
46500
46713
  #### Forge GitHub App access
46501
46714
 
@@ -47095,7 +47308,7 @@ Agents can suggest commitments and report progress. They should not create a new
47095
47308
  order: 61,
47096
47309
  title: "Signal guide",
47097
47310
  summary: "How to define and read measurables so the company runs on evidence instead of status theater.",
47098
- version: "2026-07-04.2",
47311
+ version: "2026-07-08.1",
47099
47312
  public: true,
47100
47313
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47101
47314
  stages: ["operating_rhythm"],
@@ -47158,6 +47371,10 @@ Avoid vanity numbers, manual-only status fields, and metrics nobody can act on.
47158
47371
 
47159
47372
  A human can run the whole loop from the Signal page. The page opens answer-first \u2014 one computed line ("2 of 14 measurables need attention," or "All 14 measurables on target" when nothing needs you) with only the exception rows fully visible; healthy measurables collapse to a single "N on target" count you can expand. Every create/edit action lives behind the one "+ Add" button, which opens a drawer with four segments: **Log reading** records a human reading (the same \`signal.correct_reading\` path, or a row's own "Log reading" button seeds the drawer to that measurable), **New measurable** creates one against a seat (the \`measurable.create\` path, plus adopting from the template catalog), **Sources**, and **CSV import**. You do not need an agent to keep Signal current.
47160
47373
 
47374
+ ## Per-seat budget alerts
47375
+
47376
+ The Signal page shows a seat-cost strip: each seat's trailing 30-day agent run spend. On top of that you can set a per-seat soft budget alert \u2014 a spend threshold in USD. When a seat's 30-day spend approaches its threshold (at 80%) or crosses it, a visible alert appears in the cost strip on the scorecard. This alert is advisory only: it never blocks a run or pauses an agent (hard budget enforcement is a separate, later capability). Set a threshold from the cost strip by choosing an agent seat and entering an amount, or leave the amount blank to clear it. Thresholds are per tenant and per seat; setting one records a durable event.
47377
+
47161
47378
  ## Import a scorecard from CSV
47162
47379
 
47163
47380
  To move off a spreadsheet or another operating tool, open the Signal page's "+ Add" drawer and choose the "CSV import" segment (the \`signal.import\` command, also available on CLI/MCP). Each row becomes a measurable owned by the seat whose name matches the row's owner, with its target, unit, direction, cadence, and trailing readings. Import is idempotent \u2014 re-importing upserts by period \u2014 and a row whose cadence conflicts with an existing measurable is reported and skipped, not overwritten.
@@ -47519,11 +47736,11 @@ Do not claim that a live charge happened unless Stripe test/live evidence proves
47519
47736
  order: 72,
47520
47737
  title: "Settings guide",
47521
47738
  summary: "How to use Settings as the control plane for company access, channels, providers, tokens, and operating defaults.",
47522
- version: "2026-07-04.3",
47739
+ version: "2026-07-09.2",
47523
47740
  public: true,
47524
47741
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47525
47742
  stages: ["company_setup", "staffing"],
47526
- relatedCommandIds: ["onboarding.create_invite", "mcp_token.create", "mcp_token.revoke", "mcp_token.list", "integration.list", "integration.readiness", "integration.status", "integration.test", "settings.get", "settings.update", "tenant.rename", "settings.sync_brief_scope.get", "settings.sync_brief_scope.update", "settings.product_learning.get", "settings.product_learning.update", "settings.agent_policy.get", "settings.agent_policy.update", "aicos.brain_settings.get", "aicos.brain_settings.update"],
47743
+ relatedCommandIds: ["onboarding.create_invite", "mcp_token.create", "mcp_token.revoke", "mcp_token.list", "integration.list", "integration.readiness", "integration.status", "integration.test", "settings.get", "settings.update", "tenant.rename", "settings.sync_brief_scope.get", "settings.sync_brief_scope.update", "settings.product_learning.get", "settings.product_learning.update", "settings.agent_policy.get", "settings.agent_policy.update", "settings.confirmation_policy.get", "settings.confirmation_policy.update", "aicos.brain_settings.get", "aicos.brain_settings.update"],
47527
47744
  legal: { publicRisk: "low", notes: ["{{brand}}-native settings guidance."] },
47528
47745
  sources: [
47529
47746
  {
@@ -47544,7 +47761,7 @@ Start with members and invites, then provider and channel connections, then MCP
47544
47761
 
47545
47762
  Settings is reached from the footer gear in the left sidebar (the nav collapse moved it out of the primary item list). It is a left-rail master\u2013detail layout: a grouped rail (General, Team & Access, Billing, AI & Agents, Connections, Advanced) 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.
47546
47763
 
47547
- Legacy hash deep-links still resolve \u2014 \`/settings#integrations\`, \`#channels\`, and \`#vault\` redirect to \`?section=connections\`; other hashes such as \`#members\` and \`#guardrails\` redirect to their matching section. \`#runners\` is the one exception: it keeps resolving to the dedicated Connected machines management table (not the Connections overview), since System Health and seat pages deep-link there expecting the per-runner revoke actions. 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), \`/settings/policy\` opens the Product learning section, and MCP/CLI access tokens live in the Agent access tokens section (not a separate \`/mcp\` page \u2014 \`/mcp\` is the MCP protocol endpoint itself, not a browsable settings screen). The Advanced group links out to \`/migration/ninety\`, the one settings-adjacent page with no home in the grouped rail. After you save a change, the app keeps you on the section you edited rather than returning to the top.
47764
+ Section state is fully server-side: every in-app link, redirect, and save uses \`?section=<id>\`, never a \`#anchor\` \u2014 there is no client-side hash shim to desync from. Documented shortcut pages redirect straight to the right section instead of a dead end: \`/settings/integrations\` and \`/readiness\` redirect to \`?section=connections\` (connector readiness lives inside the Connections overview, not on a standalone page), \`/settings/members\` redirects to \`?section=members\`, \`/settings/policy\` redirects to \`?section=guardrails\`, and MCP/CLI access tokens live in the Agent access tokens section (not a separate \`/mcp\` page \u2014 \`/mcp\` is the MCP protocol endpoint itself, not a browsable settings screen). The Advanced group links out to \`/migration/ninety\`, the one settings-adjacent page with no home in the grouped rail. Every settings form carries the section it lives in, so saving a change redirects back to that same section with no flash, instead of returning to the top.
47548
47765
 
47549
47766
  ## What belongs in Settings
47550
47767
 
@@ -47600,7 +47817,7 @@ Agents that run on {{brand}}-managed inference draw against a tenant inference b
47600
47817
  - Set the hard cap with \`settings.update\` (CLI: \`{{cli}} settings update --hard-cap-usd <amount>\`). The optional soft cap warns before the hard cap and must be less than or equal to it.
47601
47818
  - The sandbox dry run is free and is never blocked by the cap, so a fresh company can charter, dry-run, and take an agent live before setting a budget. The cap applies only to real managed-inference runs.
47602
47819
  - A company that brings its own provider key (BYOK) is metered on that key and is not subject to the {{brand}}-managed hard cap. BYOK changes the provider account used for eligible cloud calls, not the Charter, tool guard, human gate, or data-retention posture; see the ai-model-data-handling-guide before making provider-handling claims.
47603
- - AICOS cloud brain selection is managed with \`aicos.brain_settings.get\` and owner-gated \`aicos.brain_settings.update\`. Managed cloud answers draw from the included allowance; BYOK cloud answers meter against the tenant key. Runner Claude, Runner Codex, and MCP client answers are labeled separately and do not draw from the managed cloud allowance.
47820
+ - AICOS cloud brain selection is managed with \`aicos.brain_settings.get\` and owner-gated \`aicos.brain_settings.update\`. Managed cloud answers draw from the included allowance; BYOK cloud answers meter against the tenant key. Runner Claude and MCP client answers are labeled separately and do not draw from the managed cloud allowance. The AICOS Codex runner choice remains gated off until the governed interactive runner path is verified.
47604
47821
 
47605
47822
  ## Company autonomy ceiling (Company Guardrails)
47606
47823
 
@@ -47613,6 +47830,15 @@ The company autonomy ceiling is a single tenant-wide dial that caps how much ANY
47613
47830
  - An explicitly set ceiling is ENFORCED by default. Enforcement has two modes: \`enforce\` blocks an over-ceiling autonomous call and routes it to the steward (it is never a dead-end deny \u2014 a human can still approve), and \`observe\` allows the call but logs it. The platform default for a company that never set a policy is balanced + observe, so existing live agents are never retro-bricked.
47614
47831
  - When enforcement is on, signing a manifest or taking an agent live is REFUSED if the manifest grants always_allow to a tool whose risk exceeds the ceiling; the error names the exact ceiling, tool, and risk in its \`details\`. Lower that tool to always_ask, or raise the ceiling, then retry.
47615
47832
 
47833
+ ## CLI confirmation mode
47834
+
47835
+ The CLI confirmation mode is a separate tenant-wide dial. It does not change what an agent may do (that is the company autonomy ceiling above); it changes whether the {{cli}} CLI tool itself may stand in for a human at an already-gated confirmation.
47836
+
47837
+ - \`careful\` is the strict default for every company, including internal ones. The server refuses the CLI's silent auto-replay of a pending confirmation \u2014 a human must review it before it is approved.
47838
+ - \`trusted_operator\` is a deliberate tenant_admin opt-in (an ADR-0018 \xA76 standing authorization) that lets the CLI silently auto-approve a non-\`dangerous\` confirmation. A \`dangerous\`-risk confirmation always requires interactive human review, in either mode.
47839
+ - Read it with \`settings.confirmation_policy.get\` (CLI: \`{{cli}} settings confirmation-mode get\`). Set it with \`settings.confirmation_policy.update\` (CLI: \`{{cli}} settings confirmation-mode set --mode trusted_operator\`). Owner-only and human-gated.
47840
+ - Stored at \`tenants.settings.confirmation_policy\`; enforced server-side in \`confirmation.approve\`, so a stale or modified local CLI cannot bypass it.
47841
+
47616
47842
  ## Agent guidance
47617
47843
 
47618
47844
  Agents may explain which setting is needed and why. They should not ask users to paste secrets into chat or tool arguments. When credentials are required, route the user to the vault-backed setup flow.`
@@ -47662,7 +47888,7 @@ When a user asks to add a person, clarify whether they mean app access, seat occ
47662
47888
  order: 74,
47663
47889
  title: "Notifications guide",
47664
47890
  summary: "How {{brand}} should notify humans about decisions, escalations, stale work, and agent boundaries.",
47665
- version: "2026-06-27.1",
47891
+ version: "2026-07-09.1",
47666
47892
  public: true,
47667
47893
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47668
47894
  stages: ["operating_rhythm"],
@@ -47687,6 +47913,7 @@ Notifications should move decisions to the right human without turning {{brand}}
47687
47913
  - A Signal is broken or stale.
47688
47914
  - A Friction item needs a decision.
47689
47915
  - A Sync decision creates a handoff.
47916
+ - A seat's reporting line or parent changes (\`seat.reparent\`, a seat merge, or \`agent_setup.update\`) \u2014 the affected seat's Steward and the tenant owners get an in-app notification, so a structural change is never silent (the DER-1388 incident this closes was only caught because a human happened to notice the org chart looked wrong).
47690
47917
 
47691
47918
  ## Diagnose failed deliveries
47692
47919
 
@@ -47701,7 +47928,7 @@ Every notification should include the seat, cause, evidence, and requested decis
47701
47928
  order: 75,
47702
47929
  title: "Local runner guide",
47703
47930
  summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
47704
- version: "2026-07-07.3",
47931
+ version: "2026-07-10.1",
47705
47932
  public: true,
47706
47933
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47707
47934
  stages: ["staffing", "operating_rhythm"],
@@ -47753,11 +47980,11 @@ The runner bearer secret is stored locally and is never displayed. Each claimed
47753
47980
 
47754
47981
  ## Interactive AICOS runner turns
47755
47982
 
47756
- AICOS runner chat uses the same runner process but not the scheduled work-order queue. The runner claims an \`agent_turn_executions\` item for one user message, starts it, executes the local Claude or Codex runtime with bounded AICOS context, then reports the final assistant answer back to {{brand}}. Execute-ready AICOS claims include a server-built Seat work contract: the governed runtime profile, the AICOS context-loader tool, and the Seat permission manifest. Actions invoked from that local runtime still pass through command guards, \`tool_calls\` audit, pending confirmations, human gates, and tenant isolation before anything durable changes. The transcript write-back is server-owned: one terminal result creates one assistant message, links it to run evidence, and records the final turn status, terminal reason, timings, model, and token telemetry when present. Retrying the same final report does not append a second answer. If an interactive turn expires while queued, claimed, or running, {{brand}} marks it offline and appends a visible terminal assistant message so the user is not left waiting on a stale queued state.
47983
+ AICOS runner chat uses the same runner process but not the scheduled work-order queue. The runner claims an \`agent_turn_executions\` item for one user message, starts it, executes the local Claude runtime with bounded AICOS context, then reports the final assistant answer back to {{brand}}. Execute-ready AICOS claims include a server-built Seat work contract: the governed runtime profile, the AICOS context-loader tool, and the Seat permission manifest. Actions invoked from that local runtime still pass through command guards, \`tool_calls\` audit, pending confirmations, human gates, and tenant isolation before anything durable changes. The transcript write-back is server-owned: one terminal result creates one assistant message, links it to run evidence, and records the final turn status, terminal reason, timings, model, and token telemetry when present. Retrying the same final report does not append a second answer. Codex remains unavailable for interactive AICOS runner turns until the governed runner path is verified. If an interactive turn expires while queued, claimed, or running, {{brand}} marks it offline and appends a visible terminal assistant message so the user is not left waiting on a stale queued state.
47757
47984
 
47758
47985
  ## Forge scheduler work
47759
47986
 
47760
- When the Forge add-on is enabled, Forge phase work uses the same runner work-order lane. A Forge phase with an owning runner-agent Seat is linked to a normal \`work_orders\` row; the runner claims it, starts it, reports the result, and the server advances the linked phase. There is no separate local lease store.
47987
+ When the Forge add-on is enabled, Forge phase work uses the same runner work-order lane. A Forge phase with an owning runner-agent Seat is linked to a normal \`work_orders\` row only when a paired online runner can claim it; otherwise the phase stays pending with \`scheduler_state.status = "parked"\` and the remediation link points to \`/settings/runners/setup\`. Pairing a runner or the repair sweep re-enqueues the same pending phase idempotently, so operators do not get duplicate intake rows or silent expiring work orders. The runner claims queued work, starts it, reports the result, and the server advances the linked phase. There is no separate local lease store.
47761
47988
 
47762
47989
  A build request decomposes into changesets and tasks tracked as queryable execution state. A changeset is the pull-request boundary and the revert unit (default one per request; additional changesets only on a named split criterion such as risk isolation or a reviewability-size budget). A task is one coding session inside a changeset, carrying its own verification and a per-task checkpoint that is the safe resume and pause boundary. Plan-conformance findings loop a changeset back to implementation; a finding can only be accepted, rather than resolved, through a recorded human decision.
47763
47990
 
@@ -48639,7 +48866,7 @@ This worked document **omits** \`unanswered_boundaries\` and \`seat_type_recomme
48639
48866
  order: 43,
48640
48867
  title: "Agent builder guide",
48641
48868
  summary: "The full agent setup sequence on the CLI/MCP path \u2014 seat, steward, job, boundaries, tools, credentials, model, schedule, dry-run, go-live \u2014 with the structured model config and access tiers.",
48642
- version: "2026-06-29.1",
48869
+ version: "2026-07-10.1",
48643
48870
  public: true,
48644
48871
  audiences: ["cli", "mcp", "in_app_agent"],
48645
48872
  stages: ["staffing"],
@@ -48683,7 +48910,7 @@ Building teams of controlled agent workers is the product's core differentiator.
48683
48910
  ## The full setup sequence
48684
48911
 
48685
48912
  1. **Seat** \u2014 the agent occupies a seat in the Responsibility Graph (a function, not a person). Create or pick the seat first.
48686
- 2. **Steward** \u2014 every agent needs a human steward chain (invariant: no orphan agents). Set \`steward_seat_id\` so the chain resolves to a human; without it the occupancy is blocked.
48913
+ 2. **Steward** \u2014 every agent needs a human steward chain (invariant: no orphan agents). When the creating human occupies a qualifying seat, setup defaults the new agent's Steward to that seat and, for an unplaced fresh seat, uses it as the parent. You can still set \`steward_seat_id\` explicitly; an explicit Steward wins. Without a qualifying creator seat or explicit Steward, the occupancy is blocked and \`agent_setup.get\` returns remediation candidates.
48687
48914
  3. **Job** \u2014 what the seat owns. On \`agent.create_custom\` this is the operational answers (what it owns, what success looks like, what it must never do alone); for a strong contract, submit the full Charter directly via \`charter.update_draft\` (see the charter-authoring deep-dive).
48688
48915
  4. **Boundaries** \u2014 the Charter's \`decision_authority\` (can-do / must-ask / never / escalate), \`escalation_rules\`, and \`budget\`. Conservative by default: send/spend/irreversible actions are approval-gated or escalated.
48689
48916
  5. **Tools** \u2014 pick from the discoverable catalog (\`{{cli}} tools list\`); each tool has a default scope tier and access policy. Connect or decline via \`agent.configure_tools\`. Selecting a tool records permission; live handlers execute later only behind the signed manifest, server guard, required credential or binding, and connector-specific approval boundary.
@@ -48972,6 +49199,13 @@ var approveViaSchema = external_exports.object({
48972
49199
  var pendingConfirmationSchema2 = external_exports.object({
48973
49200
  confirmationId: external_exports.string().min(1),
48974
49201
  commandId: external_exports.string().min(1),
49202
+ // DER-1490: carried through so an inline auto-approve can always render the
49203
+ // gate before approving (D4-i). Optional/tolerant — an older/malformed
49204
+ // response still resolves confirmationId/commandId, just without the extra
49205
+ // gate detail.
49206
+ summary: external_exports.string().optional().catch(void 0),
49207
+ riskLevel: external_exports.enum(["normal", "sensitive", "dangerous"]).optional().catch(void 0),
49208
+ diff: external_exports.unknown().optional(),
48975
49209
  approveVia: approveViaSchema.optional().catch(void 0)
48976
49210
  }).passthrough();
48977
49211
  var confirmationApprovalOutputSchema = external_exports.object({
@@ -49088,16 +49322,22 @@ function brandEnvKey3(suffix) {
49088
49322
  async function renderMcpInstall(input) {
49089
49323
  const stderrLines = [];
49090
49324
  const canAutoApprove = input.canAutoApprove ?? false;
49325
+ const reviewed = input.reviewed ?? false;
49326
+ const approval = {
49327
+ canAutoApprove,
49328
+ reviewed,
49329
+ pushStderr: (line) => stderrLines.push(line)
49330
+ };
49091
49331
  if (input.options.rotateTokenId === void 0 && input.options.scope === void 0) {
49092
49332
  throw new Error(MISSING_SCOPE_MESSAGE);
49093
49333
  }
49094
- const mintScope = input.options.rotateTokenId === void 0 ? { scope: input.options.scope, ...input.options.seatId === void 0 ? {} : { seatId: input.options.seatId } } : await resolveRotationScope(input.client, input.options, canAutoApprove);
49095
- const token = await createMcpToken(input.client, mcpTokenCreateBody(mintScope, input.options.expiry), canAutoApprove);
49334
+ const mintScope = input.options.rotateTokenId === void 0 ? { scope: input.options.scope, ...input.options.seatId === void 0 ? {} : { seatId: input.options.seatId } } : await resolveRotationScope(input.client, input.options, approval);
49335
+ const token = await createMcpToken(input.client, mcpTokenCreateBody(mintScope, input.options.expiry), approval);
49096
49336
  let rotationOldRevoked = null;
49097
49337
  if (input.options.rotateTokenId !== void 0) {
49098
49338
  const oldTokenId = input.options.rotateTokenId;
49099
49339
  try {
49100
- const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId }, canAutoApprove);
49340
+ const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId }, approval);
49101
49341
  const revoke = mcpTokenRevokeOutputSchema2.safeParse(output);
49102
49342
  rotationOldRevoked = revoke.success && revoke.data.revoked === true;
49103
49343
  if (!rotationOldRevoked) {
@@ -49178,9 +49418,9 @@ function mcpTokenCreateBody(mint, expiry) {
49178
49418
  }
49179
49419
  return base;
49180
49420
  }
49181
- async function resolveRotationScope(client, options, canAutoApprove) {
49421
+ async function resolveRotationScope(client, options, approval) {
49182
49422
  const oldTokenId = options.rotateTokenId;
49183
- const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true }, canAutoApprove);
49423
+ const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true }, approval);
49184
49424
  const parsed = mcpTokenListOutputSchema2.safeParse(listOutput);
49185
49425
  if (!parsed.success) {
49186
49426
  throw new Error("Could not read the token list to rotate \u2014 mcp_token.list returned an unexpected shape.");
@@ -49243,11 +49483,11 @@ function revokeFailureReason(error51) {
49243
49483
  }
49244
49484
  return "unknown error";
49245
49485
  }
49246
- async function createMcpToken(client, body, canAutoApprove) {
49247
- const output = await executeWithConfirmation(client, "mcp_token.create", body, canAutoApprove);
49486
+ async function createMcpToken(client, body, approval) {
49487
+ const output = await executeWithConfirmation(client, "mcp_token.create", body, approval);
49248
49488
  return mcpTokenOutputSchema.parse(output);
49249
49489
  }
49250
- async function executeWithConfirmation(client, commandId, body, canAutoApprove) {
49490
+ async function executeWithConfirmation(client, commandId, body, approval) {
49251
49491
  try {
49252
49492
  const result = await client.execute(commandId, body);
49253
49493
  return result.output;
@@ -49256,7 +49496,7 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
49256
49496
  if (!pending) {
49257
49497
  throw error51;
49258
49498
  }
49259
- if (!canAutoApprove) {
49499
+ if (!approval.canAutoApprove) {
49260
49500
  throw new PendingConfirmationError({
49261
49501
  confirmationId: pending.confirmationId,
49262
49502
  commandId: pending.commandId,
@@ -49264,13 +49504,33 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
49264
49504
  ...pending.approveVia?.webUrl === void 0 ? {} : { approveUrl: pending.approveVia.webUrl }
49265
49505
  });
49266
49506
  }
49507
+ approval.pushStderr(renderPendingConfirmationGate(pending));
49267
49508
  const approved = await client.execute("confirmation.approve", {
49268
- confirmation_id: pending.confirmationId
49509
+ confirmation_id: pending.confirmationId,
49510
+ // DER-1490: never claim a review that didn't happen — omit the field
49511
+ // entirely rather than sending `reviewed: false` (the server treats
49512
+ // "absent" and "false" the same, but an absent field is honest about there
49513
+ // being no signal either way).
49514
+ ...approval.reviewed ? { reviewed: true } : {}
49269
49515
  });
49270
49516
  const approvalOutput = confirmationApprovalOutputSchema.parse(approved.output);
49271
49517
  return approvalOutput.output;
49272
49518
  }
49273
49519
  }
49520
+ function renderPendingConfirmationGate(pending) {
49521
+ const lines = [`Confirmation required: ${pending.commandId} (${pending.confirmationId})`];
49522
+ if (pending.summary !== void 0) {
49523
+ lines.push(redactForLog(pending.summary));
49524
+ }
49525
+ if (pending.riskLevel !== void 0) {
49526
+ lines.push(`Risk level: ${pending.riskLevel}`);
49527
+ }
49528
+ if (pending.diff !== void 0) {
49529
+ lines.push(`Diff:
49530
+ ${redactForLog(pending.diff)}`);
49531
+ }
49532
+ return lines.join("\n");
49533
+ }
49274
49534
  function pendingCommandConfirmation(error51, commandId) {
49275
49535
  if (!(error51 instanceof CommandClientError) || error51.status !== 409 || !error51.response || error51.response.ok) {
49276
49536
  return null;
@@ -49545,7 +49805,7 @@ var COMMAND_MANIFEST = [
49545
49805
  { "id": "agent_setup.get", "namespace": "agent_setup", "action": "get", "title": "Get agent setup", "description": "Read the resumable agent-setup state: mode, parent, steward, template, lane, schedule, tool decisions, dry run, and next action.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": false }, { "name": "setup_id", "flag": "setup-id", "type": "string", "required": false }], "hasComplexInput": false, "help": "Read the resumable setup state: mode, parent, steward, lane, schedule, tool decisions, dry-run blockers, and the next action." },
49546
49806
  { "id": "agent_setup.relane", "namespace": "agent_setup", "action": "relane", "title": "Re-lane a live agent", "description": "Governed re-lane of a live agent: pause, move to the new lane, force a fresh dry-run rehearsal on that lane, then go live. Owner-only and human-gated; records a rationale.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "lane", "flag": "lane", "type": "enum", "required": true, "enumValues": ["cloud", "mcp_session", "runner"] }, { "name": "reason", "flag": "reason", "type": "string", "required": true }], "hasComplexInput": false, "help": "Re-lane a live agent (cloud / runner / mcp_session): it pauses, moves to the new lane, forces a fresh dry-run rehearsal on that lane, then goes live \u2014 owner-only, human-gated, with a required rationale. A runner target needs a paired runner; a failed rehearsal leaves the agent paused on the new lane." },
49547
49807
  { "id": "agent_setup.start", "namespace": "agent_setup", "action": "start", "title": "Start agent setup", "description": "Start (or resume) a draft agent setup for a seat in template, custom, or existing mode.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "mode", "flag": "mode", "type": "enum", "required": true, "enumValues": ["template", "custom", "existing"] }, { "name": "template_slug", "flag": "template-slug", "type": "string", "required": false }], "hasComplexInput": false, "help": "Start a draft agent setup from a template, custom, or existing mode; the setup resumes from the draft agent and Charter, not a fresh start." },
49548
- { "id": "agent_setup.update", "namespace": "agent_setup", "action": "update", "title": "Update agent setup", "description": "Update the draft agent's parent, steward, lane, schedule, operational answers, and tool decisions.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "parent_seat_id", "flag": "parent-seat-id", "type": "string", "required": false }, { "name": "steward_seat_id", "flag": "steward-seat-id", "type": "string", "required": false }, { "name": "lane", "flag": "lane", "type": "enum", "required": false, "enumValues": ["cloud", "mcp_session", "runner"] }, { "name": "schedule_cron", "flag": "schedule-cron", "type": "string", "required": false }, { "name": "answers", "flag": "answers", "type": "array", "required": false, "itemType": "string" }], "hasComplexInput": true, "help": "Update parent, steward, lane, schedule, answers, or tool decisions on the draft; steward and parent changes keep the no-orphan chain explicit." },
49808
+ { "id": "agent_setup.update", "namespace": "agent_setup", "action": "update", "title": "Update agent setup", "description": "Update the draft agent's parent, steward, lane, schedule, operational answers, and tool decisions.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "parent_seat_id", "flag": "parent-seat-id", "type": "string", "required": false }, { "name": "steward_seat_id", "flag": "steward-seat-id", "type": "string", "required": false }, { "name": "lane", "flag": "lane", "type": "enum", "required": false, "enumValues": ["cloud", "mcp_session", "runner"] }, { "name": "schedule_cron", "flag": "schedule-cron", "type": "string", "required": false }, { "name": "answers", "flag": "answers", "type": "array", "required": false, "itemType": "string" }, { "name": "skill_discovery_enabled", "flag": "skill-discovery-enabled", "type": "boolean", "required": false }], "hasComplexInput": true, "help": "Update parent, steward, lane, schedule, answers, or tool decisions on the draft; steward and parent changes keep the no-orphan chain explicit." },
49549
49809
  { "id": "agent_template.list", "namespace": "agent_template", "action": "list", "title": "List agent templates", "description": "List stock agent templates and their responsibilities, default tools, and dry-run rehearsal.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "List stock agent templates and their default tools and safety boundaries before starting a template setup." },
49550
49810
  { "id": "agent.configure_tools", "namespace": "agent", "action": "configure_tools", "title": "Configure agent tools", "description": "Save tool proposals, declines, and credential-ingress requests for a draft agent. Never stores or echoes secret material; vault refs only.", "requiredScope": "seat", "confirmation": "credential_flow", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }], "hasComplexInput": true, "help": "Connect or decline proposed tools and stage credential-ingress requests; never place raw secrets here \u2014 secrets flow through vault-backed credential.ingress as vault refs only.", "example": { "seat_id": "0190aaaa-aaaa-7aaa-8aaa-aaaaaaaaaaaa", "tool_decisions": [{ "tool": "ap.invoices.read", "decision": "connect" }, { "tool": "email.draft", "decision": "connect" }] } },
49551
49811
  { "id": "agent.create_custom", "namespace": "agent", "action": "create_custom", "title": "Create custom agent", "description": "Create a draft custom agent shell and a draft Charter seed from operational answers (what it owns, success, never-do-alone, steward).", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "steward_seat_id", "flag": "steward-seat-id", "type": "string", "required": false }, { "name": "lane", "flag": "lane", "type": "enum", "required": false, "enumValues": ["cloud", "mcp_session", "runner"] }, { "name": "what_it_owns", "flag": "what-it-owns", "type": "string", "required": false }, { "name": "what_success_looks_like", "flag": "what-success-looks-like", "type": "string", "required": false }, { "name": "what_it_must_never_do_alone", "flag": "what-it-must-never-do-alone", "type": "string", "required": false }], "hasComplexInput": true, "help": "Create a draft custom agent from operational answers (what it owns, success, never-do-alone, steward); the Charter Builder owns the contract and go-live stays human-gated.", "example": { "seat_id": "0190aaaa-aaaa-7aaa-8aaa-aaaaaaaaaaaa", "steward_seat_id": "0190bbbb-bbbb-7bbb-8bbb-bbbbbbbbbbbb", "lane": "cloud", "what_it_owns": "Drafting AP invoice entries from inbound vendor emails for human review.", "what_success_looks_like": "Every inbound invoice is drafted within one business day with the correct GL coding.", "what_it_must_never_do_alone": "Never send payment or approve an invoice without a human sign-off." } },
@@ -49592,9 +49852,9 @@ var COMMAND_MANIFEST = [
49592
49852
  { "id": "compass.set", "namespace": "compass", "action": "set", "title": "Set Compass", "description": "Compatibility command for the DER-626 MCP Compass tool.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "draft_id", "flag": "draft-id", "type": "string", "required": false }, { "name": "approve", "flag": "approve", "type": "boolean", "required": false }], "hasComplexInput": true, "help": "Set Compass drafts in the product's own vocabulary and approve only with explicit human confirmation." },
49593
49853
  { "id": "compass.show_markdown", "namespace": "compass", "action": "show_markdown", "title": "Show Compass as markdown", "description": "Compose the current Compass and its open gaps into a clean, human-skimmable markdown card for review.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Render the current Compass and its open gaps as a clean markdown card to show your human a quick review." },
49594
49854
  { "id": "compass.update_draft", "namespace": "compass", "action": "update_draft", "title": "Update Compass draft", "description": "Replace a draft Compass document.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "draft_id", "flag": "draft-id", "type": "string", "required": true }], "hasComplexInput": true, "help": "Pass the compass_version_id returned by compass.draft as draft_id; update_draft replaces only an unpublished draft in this tenant." },
49595
- { "id": "confirmation.approve", "namespace": "confirmation", "action": "approve", "title": "Approve pending confirmation", "description": "Approve an in-flight confirmation and execute the original command as the approving human.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Approve pending confirmations only when the requested durable change is clear to the human owner." },
49855
+ { "id": "confirmation.approve", "namespace": "confirmation", "action": "approve", "title": "Approve pending confirmation", "description": "Approve an in-flight confirmation and execute the original command as the approving human.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }, { "name": "reviewed", "flag": "reviewed", "type": "boolean", "required": false }], "hasComplexInput": false, "help": "Approve pending confirmations only when the requested durable change is clear to the human owner." },
49596
49856
  { "id": "confirmation.reject", "namespace": "confirmation", "action": "reject", "title": "Reject pending confirmation", "description": "Reject an in-flight confirmation without executing the original command.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }, { "name": "reason", "flag": "reason", "type": "string", "required": false }], "hasComplexInput": false, "help": "Reject pending confirmations when authority, evidence, or human intent is unclear." },
49597
- { "id": "credential.ingress", "namespace": "credential", "action": "ingress", "title": "Store credential", "description": "Store a secret through the vault and persist only the credential vault reference.", "requiredScope": "seat", "confirmation": "credential_flow", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": false }, { "name": "provider", "flag": "provider", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }], "hasComplexInput": false, "secretBlocked": true, "help": "Ingress credentials only through vault-backed flows; never place raw secrets in prompts, logs, or docs." },
49857
+ { "id": "credential.ingress", "namespace": "credential", "action": "ingress", "title": "Store credential", "description": "Store a secret through the vault and persist only the credential vault reference.", "requiredScope": "seat", "confirmation": "credential_flow", "exposeOverMcp": false, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": false }, { "name": "provider", "flag": "provider", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }], "hasComplexInput": false, "secretBlocked": true, "help": "Ingress credentials only through vault-backed flows; never place raw secrets in prompts, logs, or docs." },
49598
49858
  { "id": "deliverable.accept", "namespace": "deliverable", "action": "accept", "title": "Accept deliverable value", "description": "Record a human-confirmed accepted value (USD) on a deliverable. Humans decide; agents cannot self-accept. Re-accepting records a correction as a new event; prior events are never mutated.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "deliverable_id", "flag": "deliverable-id", "type": "string", "required": true }, { "name": "impact_value_usd", "flag": "impact-value-usd", "type": "number", "required": true }], "hasComplexInput": false, "help": "Record a human-confirmed accepted value (USD) on a deliverable; owner-only and human-gated. Humans decide \u2014 agents cannot self-accept. Re-accepting records a correction as a new event." },
49599
49859
  { "id": "deliverable.attach", "namespace": "deliverable", "action": "attach", "title": "Attach agent deliverable", "description": "Attach a deliverable to a source run, task, or work order for the acting seat. Source refs are validated server-side.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "run_id", "flag": "run-id", "type": "string", "required": false }, { "name": "task_id", "flag": "task-id", "type": "string", "required": false }, { "name": "work_order_id", "flag": "work-order-id", "type": "string", "required": false }, { "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["brief", "work_evidence", "artifact", "report", "other"] }, { "name": "title", "flag": "title", "type": "string", "required": true }, { "name": "summary", "flag": "summary", "type": "string", "required": false }, { "name": "content", "flag": "content", "type": "string", "required": false }], "hasComplexInput": true, "help": "Attach a scrubbed deliverable to a source run, task, or work order that belongs to the acting seat." },
49600
49860
  { "id": "deliverable.create", "namespace": "deliverable", "action": "create", "title": "Create agent deliverable", "description": "Create an agent deliverable for the acting seat. The deliverable is scrubbed for secrets before storage.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["brief", "work_evidence", "artifact", "report", "other"] }, { "name": "title", "flag": "title", "type": "string", "required": true }, { "name": "summary", "flag": "summary", "type": "string", "required": false }, { "name": "content", "flag": "content", "type": "string", "required": false }, { "name": "delivered_at", "flag": "delivered-at", "type": "string", "required": false }], "hasComplexInput": true, "help": "Create a scrubbed durable work output for the acting seat without making a durable company decision." },
@@ -49627,7 +49887,7 @@ var COMMAND_MANIFEST = [
49627
49887
  { "id": "goal.unbind_measurable", "namespace": "goal", "action": "unbind_measurable", "title": "Unbind a Signal from a Cascade goal", "description": "Remove a measurable's drives_status binding from a goal. The goal keeps its last status; unbinding only stops future signal-driven updates.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "goal_id", "flag": "goal-id", "type": "string", "required": true }, { "name": "measurable_id", "flag": "measurable-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Remove a Signal's drives_status binding from a goal; the goal keeps its last status and stops receiving signal-driven updates." },
49628
49888
  { "id": "goal.update", "namespace": "goal", "action": "update", "title": "Update Cascade goal", "description": "Update a goal's title or definition of done. Topology (parent/seat) and status have dedicated commands.", "requiredScope": "tenant_admin", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "goal_id", "flag": "goal-id", "type": "string", "required": true }, { "name": "title", "flag": "title", "type": "string", "required": false }, { "name": "definition_of_done", "flag": "definition-of-done", "type": "string", "required": false }], "hasComplexInput": false, "help": "Update a goal's title or definition of done; topology and status have dedicated commands." },
49629
49889
  { "id": "graph.get", "namespace": "graph", "action": "get", "title": "Get Responsibility Graph", "description": "Return the tenant's Responsibility Graph: seats, edges, root, occupants, and status rollups.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read the Responsibility Graph to discover seat ids, parentage, occupants, and status before mutating or routing work." },
49630
- { "id": "integration.connect_rest", "namespace": "integration", "action": "connect_rest", "title": "Connect REST integration", "description": "Create or rotate the tenant Baserow REST integration using a vault-backed secret. Stores endpoint metadata only; never returns secrets or vault refs.", "requiredScope": "tenant_admin", "confirmation": "credential_flow", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": true, "enumValues": ["baserow"] }, { "name": "endpoint_url", "flag": "endpoint-url", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }, { "name": "account_label", "flag": "account-label", "type": "string", "required": false }], "hasComplexInput": false, "secretBlocked": true, "help": "Create or rotate a vault-backed Baserow REST integration for Signal pulls. Use the credential flow; never pass secrets through CLI argv." },
49890
+ { "id": "integration.connect_rest", "namespace": "integration", "action": "connect_rest", "title": "Connect REST integration", "description": "Create or rotate the tenant Baserow REST integration using a vault-backed secret. Stores endpoint metadata only; never returns secrets or vault refs.", "requiredScope": "tenant_admin", "confirmation": "credential_flow", "exposeOverMcp": false, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": true, "enumValues": ["baserow"] }, { "name": "endpoint_url", "flag": "endpoint-url", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }, { "name": "account_label", "flag": "account-label", "type": "string", "required": false }], "hasComplexInput": false, "secretBlocked": true, "help": "Create or rotate a vault-backed Baserow REST integration for Signal pulls. Use the credential flow; never pass secrets through CLI argv." },
49631
49891
  { "id": "integration.discover", "namespace": "integration", "action": "discover", "title": "Discover a connection for signals", "description": "Read-only introspection of a connected source for signal binding. Returns non-secret adapter metadata and a rest-for-signals recipe skeleton to author against signal.preview. Exposes no secrets.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "integration_id", "flag": "integration-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Read-only introspection of a connected source: returns non-secret metadata + a recipe skeleton to author a signal pull against. No network." },
49632
49892
  { "id": "integration.list", "namespace": "integration", "action": "list", "title": "List integrations", "description": "List tenant integration metadata and health status. Returns account/scopes/capabilities only; never secrets or vault refs.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "string", "required": false }], "hasComplexInput": false, "help": "List connected integrations and their metadata-only health state before enabling a connector-backed tool." },
49633
49893
  { "id": "integration.readiness", "namespace": "integration", "action": "readiness", "title": "Check integration readiness", "description": "Return a safe connector setup checklist for operators and agents. Starts with Google and exposes no secrets.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": false, "enumValues": ["google"] }], "hasComplexInput": false, "help": "Read the connector setup checklist before relying on Google-backed agent work; the checklist names missing config, connection, scopes, external verification, and handler blockers." },
@@ -49669,6 +49929,8 @@ var COMMAND_MANIFEST = [
49669
49929
  { "id": "seat.set_type", "namespace": "seat", "action": "set_type", "title": "Set seat type", "description": "Set a Responsibility Graph seat type.", "requiredScope": "seat", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "seat_type", "flag": "seat-type", "type": "enum", "required": true, "enumValues": ["human", "agent", "hybrid"] }], "hasComplexInput": false, "help": "Seat type should follow the work, risk, measurable, and stewardship model." },
49670
49930
  { "id": "settings.agent_policy.get", "namespace": "settings", "action": "agent_policy.get", "title": "Get company autonomy ceiling", "description": "Read this company's Company Guardrails ceiling (ADR-0007): the profile, enforcement mode, and the most an agent may do autonomously (max_autonomous_risk). Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read this company's autonomy ceiling (Company Guardrails, ADR-0007): profile, enforcement mode, and the most an agent may do autonomously." },
49671
49931
  { "id": "settings.agent_policy.update", "namespace": "settings", "action": "agent_policy.update", "title": "Set company autonomy ceiling", "description": "Set the Company Guardrails ceiling (ADR-0007): a named profile (locked_down, balanced, high_autonomy) or custom + max_autonomous_risk. Tenant-admin and human-gated. An explicitly set ceiling is ENFORCED by default; pass enforcement=observe to stage a change without blocking.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "profile", "flag": "profile", "type": "enum", "required": true, "enumValues": ["locked_down", "balanced", "high_autonomy", "custom"] }, { "name": "max_autonomous_risk", "flag": "max-autonomous-risk", "type": "enum", "required": false, "enumValues": ["low", "medium", "high", "critical"] }, { "name": "enforcement", "flag": "enforcement", "type": "enum", "required": false, "enumValues": ["observe", "enforce"] }], "hasComplexInput": false, "help": "Set the company autonomy ceiling: a profile (locked_down, balanced, high_autonomy) or custom + max_autonomous_risk. Owner-only, human-gated, and enforced by default." },
49932
+ { "id": "settings.confirmation_policy.get", "namespace": "settings", "action": "confirmation_policy.get", "title": "Get CLI confirmation mode", "description": "Read this company's CLI confirmation mode (DER-1490): careful (the strict default \u2014 the CLI cannot silently auto-approve a confirmation) or trusted_operator (an owner opt-in permitting silent auto-approval of non-dangerous confirmations). Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read this company's CLI confirmation mode: careful (the strict default \u2014 the CLI cannot silently auto-approve a confirmation) or trusted_operator (an owner opt-in permitting silent auto-approval of non-dangerous confirmations)." },
49933
+ { "id": "settings.confirmation_policy.update", "namespace": "settings", "action": "confirmation_policy.update", "title": "Set CLI confirmation mode", "description": "Set this company's CLI confirmation mode (DER-1490): careful (strict \u2014 no silent CLI auto-approval) or trusted_operator (owner opt-in permitting silent auto-approval of non-dangerous confirmations; dangerous confirmations always require an interactive review). Owner-only and human-gated: choosing trusted_operator establishes an ADR-0018 \xA76 standing authorization.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "cli_mode", "flag": "cli-mode", "type": "enum", "required": true, "enumValues": ["trusted_operator", "careful"] }], "hasComplexInput": false, "help": "Set the CLI confirmation mode to careful or trusted_operator. Owner-only and human-gated; trusted_operator establishes a standing authorization and dangerous confirmations still require an interactive review." },
49672
49934
  { "id": "settings.get", "namespace": "settings", "action": "get", "title": "Get tenant settings", "description": "Read tenant operating settings: plan, status, spend/budgets, integration status, and notification preferences. Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read tenant operating settings: plan, spend, budgets, integration status, and notification preferences. No secrets." },
49673
49935
  { "id": "settings.product_learning.get", "namespace": "settings", "action": "product_learning.get", "title": "Get product-learning policy", "description": "Read whether product/page/recommendation analytics may be recorded for this tenant. Security and audit logs remain enabled in every mode.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read whether product/page/recommendation analytics are allowed for the tenant; security and audit logs always stay enabled." },
49674
49936
  { "id": "settings.product_learning.update", "namespace": "settings", "action": "product_learning.update", "title": "Update product-learning policy", "description": "Set tenant product-learning participation. Disabled and enterprise-contract modes block generic product analytics writes, but not security/audit logs.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "mode", "flag": "mode", "type": "enum", "required": true, "enumValues": ["enabled", "disabled", "enterprise_contract"] }], "hasComplexInput": false, "help": "Set tenant product-learning mode to enabled, disabled, or enterprise_contract; the change is human-gated." },
@@ -50646,6 +50908,14 @@ function renderAgentPolicySet(output) {
50646
50908
  const policy = asRecord(asRecord(output).agent_policy);
50647
50909
  return `Set company autonomy ceiling to ${field(policy, "profile")} (max_autonomous_risk=${field(policy, "max_autonomous_risk")}, enforcement=${field(policy, "enforcement")}).`;
50648
50910
  }
50911
+ function renderConfirmationModeGet(output) {
50912
+ const policy = asRecord(asRecord(output).confirmation_policy);
50913
+ return `confirmation_policy cli_mode=${field(policy, "cli_mode")} explicit=${field(policy, "explicit")}`;
50914
+ }
50915
+ function renderConfirmationModeSet(output) {
50916
+ const policy = asRecord(asRecord(output).confirmation_policy);
50917
+ return `Updated CLI confirmation mode to ${field(policy, "cli_mode")}.`;
50918
+ }
50649
50919
  function renderMemberInvite(output) {
50650
50920
  const record2 = asRecord(output);
50651
50921
  return `Invited ${field(record2, "email")} as ${field(record2, "role")} (invite ${field(record2, "invite_id")}).`;
@@ -51209,6 +51479,13 @@ var agentPolicySetHandler = bespoke("settings.agent_policy.update", (parsed) =>
51209
51479
  enforcement: optionalValue(parsed, "enforcement")
51210
51480
  });
51211
51481
  }, renderAgentPolicySet);
51482
+ var confirmationModeSetHandler = bespoke("settings.confirmation_policy.update", (parsed) => {
51483
+ const mode = optionalValue(parsed, "mode");
51484
+ if (mode !== "careful" && mode !== "trusted_operator") {
51485
+ throw new UsageError("Provide --mode careful|trusted_operator.");
51486
+ }
51487
+ return { cli_mode: mode };
51488
+ }, renderConfirmationModeSet);
51212
51489
  var seatDecommissionHandler = (context, rest) => {
51213
51490
  const parsed = parseFlags(rest, /* @__PURE__ */ new Set(["dry-run", "preview", "yes", "y"]));
51214
51491
  if (parsed.flags.has("dry-run") || parsed.flags.has("preview")) {
@@ -51230,6 +51507,13 @@ var settingsAgentPolicyHandler = (context, rest) => dispatch(context, "settings
51230
51507
  },
51231
51508
  set: (ctx, r) => agentPolicySetHandler(ctx, r)
51232
51509
  }, `Usage: ${context.binName} settings agent-policy get|set --profile locked_down|balanced|high_autonomy|custom [--max-autonomous-risk <r>] [--enforcement enforce|observe]`);
51510
+ var settingsConfirmationModeHandler = (context, rest) => dispatch(context, "settings confirmation-mode", rest, {
51511
+ get: (ctx, r) => {
51512
+ const parsed = parseFlags(r, /* @__PURE__ */ new Set(["yes", "y"]));
51513
+ return execute(ctx, parsed, "settings.confirmation_policy.get", {}, renderConfirmationModeGet);
51514
+ },
51515
+ set: (ctx, r) => confirmationModeSetHandler(ctx, r)
51516
+ }, `Usage: ${context.binName} settings confirmation-mode get|set --mode careful|trusted_operator`);
51233
51517
  var agentSetupHandler = (context, rest) => dispatch(context, "agent setup", rest, {
51234
51518
  status: (ctx, r) => {
51235
51519
  const parsed = parseFlags(r, /* @__PURE__ */ new Set(["yes", "y"]));
@@ -51408,6 +51692,7 @@ var GROUP_SPECS = {
51408
51692
  update: { commandId: "settings.update", handler: settingsUpdateHandler, render: renderSettingsUpdate },
51409
51693
  "product-learning": { commandId: "settings.product_learning.get", handler: settingsProductLearningHandler },
51410
51694
  "agent-policy": { commandId: "settings.agent_policy.get", handler: settingsAgentPolicyHandler },
51695
+ "confirmation-mode": { commandId: "settings.confirmation_policy.get", handler: settingsConfirmationModeHandler },
51411
51696
  rename: { commandId: "tenant.rename", handler: settingsRenameHandler, render: renderTenantRename }
51412
51697
  }
51413
51698
  },
@@ -51524,6 +51809,8 @@ var EXTRA_SURFACED = /* @__PURE__ */ new Set([
51524
51809
  // settings product-learning update
51525
51810
  "settings.agent_policy.update",
51526
51811
  // settings agent-policy set
51812
+ "settings.confirmation_policy.update",
51813
+ // settings confirmation-mode set
51527
51814
  "skill.import_github"
51528
51815
  // skills import github
51529
51816
  ]);
@@ -51627,7 +51914,7 @@ function operationUsageLines(bin) {
51627
51914
  `${bin} runner install-service|start|stop|restart|status|logs|uninstall --name <name>`,
51628
51915
  `${bin} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume`,
51629
51916
  `${bin} notification settings|test|errors`,
51630
- `${bin} settings get|update|product-learning|agent-policy|rename`,
51917
+ `${bin} settings get|update|product-learning|agent-policy|confirmation-mode|rename`,
51631
51918
  `${bin} member invite|update|remove`,
51632
51919
  `${bin} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show`,
51633
51920
  `${bin} tools list`,
@@ -51945,9 +52232,10 @@ async function executeSkillCommand(context, commandId, body, options = {}) {
51945
52232
  } catch (error51) {
51946
52233
  const pending = context.io.interactive === true ? pendingConfirmationFromError(error51) : null;
51947
52234
  if (pending && pending.commandId === commandId) {
52235
+ renderPendingConfirmationGate2(context.io, pending);
51948
52236
  context.io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the interactive CLI user.
51949
52237
  `);
51950
- const approved = await context.client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
52238
+ const approved = await context.client.execute("confirmation.approve", { confirmation_id: pending.confirmationId, reviewed: true });
51951
52239
  return asRecord2(approved.output).output ?? approved.output;
51952
52240
  }
51953
52241
  throw error51;
@@ -52193,7 +52481,30 @@ function pendingConfirmationFromError(error51) {
52193
52481
  if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
52194
52482
  return null;
52195
52483
  }
52196
- return { confirmationId, commandId };
52484
+ const summary = typeof details.summary === "string" ? details.summary : void 0;
52485
+ const riskLevel = typeof details.riskLevel === "string" ? details.riskLevel : void 0;
52486
+ return {
52487
+ confirmationId,
52488
+ commandId,
52489
+ ...summary === void 0 ? {} : { summary },
52490
+ ...riskLevel === void 0 ? {} : { riskLevel },
52491
+ ...details.diff === void 0 ? {} : { diff: details.diff }
52492
+ };
52493
+ }
52494
+ function renderPendingConfirmationGate2(io, pending) {
52495
+ if (pending.summary !== void 0) {
52496
+ io.stderr.write(`${redactForLog(pending.summary)}
52497
+ `);
52498
+ }
52499
+ if (pending.riskLevel !== void 0) {
52500
+ io.stderr.write(`Risk level: ${pending.riskLevel}
52501
+ `);
52502
+ }
52503
+ if (pending.diff !== void 0) {
52504
+ io.stderr.write(`Diff:
52505
+ ${redactForLog(pending.diff)}
52506
+ `);
52507
+ }
52197
52508
  }
52198
52509
 
52199
52510
  // src/doctor.ts
@@ -52202,12 +52513,286 @@ import { access as access2 } from "node:fs/promises";
52202
52513
  import { delimiter } from "node:path";
52203
52514
  import { promisify as promisify3 } from "node:util";
52204
52515
 
52516
+ // src/runner-sandbox.ts
52517
+ import { existsSync as existsSync2, realpathSync } from "node:fs";
52518
+ import { readdir, rm as rm2, stat as stat2 } from "node:fs/promises";
52519
+ import { tmpdir } from "node:os";
52520
+ import path3 from "node:path";
52521
+ var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
52522
+ var RUNNER_ASKPASS_DIR = path3.join(tmpdir(), "rost-runner-askpass");
52523
+ var RUNNER_TEMP_PREFIXES = [
52524
+ "rost-runner-mcp-",
52525
+ "rost-runner-sandbox-",
52526
+ "rost-runner-askpass-"
52527
+ ];
52528
+ var RUNNER_TEMP_STARTUP_SWEEP_MAX_AGE_MS = 60 * 60 * 1e3;
52529
+ var RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS = 45 * 60 * 1e3;
52530
+ var SENSITIVE_READ_SUBPATHS = [
52531
+ ".ssh",
52532
+ ".aws",
52533
+ ".gnupg",
52534
+ ".config/gcloud",
52535
+ ".config/gh",
52536
+ ".kube",
52537
+ ".docker",
52538
+ ".npmrc"
52539
+ ];
52540
+ var SENSITIVE_READ_LITERALS = [".netrc"];
52541
+ var SENSITIVE_WRITE_SUBPATHS = [
52542
+ ".ssh",
52543
+ ".aws",
52544
+ ".gnupg",
52545
+ ".config/git",
52546
+ "bin",
52547
+ "Library/LaunchAgents"
52548
+ ];
52549
+ var SENSITIVE_WRITE_LITERALS = [
52550
+ ".gitconfig",
52551
+ ".zshrc",
52552
+ ".zprofile",
52553
+ ".zshenv",
52554
+ ".zlogin",
52555
+ ".bashrc",
52556
+ ".bash_profile",
52557
+ ".bash_login",
52558
+ ".profile"
52559
+ ];
52560
+ function parseSandboxMode(value) {
52561
+ const normalized = (value ?? "auto").trim().toLowerCase();
52562
+ if (normalized === "off" || normalized === "strict" || normalized === "auto" || normalized === "") {
52563
+ return normalized === "" ? "auto" : normalized;
52564
+ }
52565
+ return "auto";
52566
+ }
52567
+ function parseSandboxNetwork(value) {
52568
+ const normalized = (value ?? "").trim().toLowerCase();
52569
+ if (normalized === "" || normalized === "allow") {
52570
+ return { allowNetwork: true, unrecognized: null };
52571
+ }
52572
+ if (normalized === "deny") {
52573
+ return { allowNetwork: false, unrecognized: null };
52574
+ }
52575
+ return { allowNetwork: true, unrecognized: value ?? "" };
52576
+ }
52577
+ function sbplString(value) {
52578
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
52579
+ }
52580
+ function subpath(value) {
52581
+ return `(subpath ${sbplString(value)})`;
52582
+ }
52583
+ function literal2(value) {
52584
+ return `(literal ${sbplString(value)})`;
52585
+ }
52586
+ var CLAUDE_TEMP_SBPL_REGEX = '(regex #"^/private/tmp/claude-[^/]+")';
52587
+ var STRICT_SYSTEM_READ_SUBPATHS = [
52588
+ "/usr",
52589
+ "/bin",
52590
+ "/sbin",
52591
+ "/System",
52592
+ "/Library",
52593
+ "/private/etc",
52594
+ "/private/var/db",
52595
+ "/dev",
52596
+ "/opt",
52597
+ "/nix"
52598
+ ];
52599
+ function buildSeatbeltProfile(input) {
52600
+ const home = input.homeDir.replace(/\/+$/, "");
52601
+ const lines = ["(version 1)"];
52602
+ const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
52603
+ const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
52604
+ const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
52605
+ const denyWriteLiterals = input.denyWritePaths.filter((p) => !p.endsWith("/")).map(literal2);
52606
+ const credentialReadDenies = [
52607
+ ...SENSITIVE_READ_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
52608
+ ...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
52609
+ ];
52610
+ if (input.strict) {
52611
+ lines.push("(deny default)");
52612
+ lines.push("(allow process*)");
52613
+ lines.push("(allow sysctl-read)");
52614
+ lines.push("(allow mach-lookup)");
52615
+ lines.push("(allow iokit-open)");
52616
+ lines.push("(allow system-socket)");
52617
+ lines.push("(allow signal (target self))");
52618
+ lines.push("(allow file-read-metadata)");
52619
+ const strictReads = [
52620
+ // The root directory NODE itself (not its children — `literal`, never `subpath`).
52621
+ // dyld resolves the `/System/Volumes/Preboot/Cryptexes` firmlink to load the arm64
52622
+ // shared cache at process start, and that resolution reads `/`; without this a
52623
+ // deny-default profile SIGABRTs every spawned runtime (cat/node/claude) before it
52624
+ // runs. `(literal "/")` grants only the root dir listing/metadata (no secret), while
52625
+ // every child path stays individually deny-gated. (Guard never hits this branch.)
52626
+ literal2("/"),
52627
+ ...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
52628
+ subpath(`${home}/.claude`),
52629
+ subpath(`${home}/.codex`),
52630
+ subpath(input.tmpDir)
52631
+ ];
52632
+ lines.push(`(allow file-read* ${strictReads.join(" ")})`);
52633
+ } else {
52634
+ lines.push("(allow default)");
52635
+ }
52636
+ lines.push("(deny appleevent-send)");
52637
+ if (denyReadBaseSubpaths.length > 0) {
52638
+ lines.push(`(deny file-read* ${denyReadBaseSubpaths.join(" ")})`);
52639
+ }
52640
+ const workspaceReads = [subpath(input.workspaceDir), ...input.allowWritePaths.map(subpath), CLAUDE_TEMP_SBPL_REGEX];
52641
+ lines.push(`(allow file-read* ${workspaceReads.join(" ")})`);
52642
+ lines.push("(deny file-write*)");
52643
+ const allowWrites = [
52644
+ subpath(input.workspaceDir),
52645
+ subpath(input.tmpDir),
52646
+ subpath(`${home}/.claude`),
52647
+ subpath(`${home}/.codex`),
52648
+ ...input.allowWritePaths.map(subpath),
52649
+ CLAUDE_TEMP_SBPL_REGEX,
52650
+ // DER-1398: Claude Code's per-session + per-command temp dirs.
52651
+ subpath("/dev/fd"),
52652
+ literal2("/dev/null"),
52653
+ literal2("/dev/stdout"),
52654
+ literal2("/dev/stderr"),
52655
+ literal2("/dev/tty"),
52656
+ literal2("/dev/dtracehelper")
52657
+ ];
52658
+ lines.push(`(allow file-write* ${allowWrites.join(" ")})`);
52659
+ const lastReadDenies = [
52660
+ ...credentialReadDenies,
52661
+ ...denyReadLiterals,
52662
+ ...input.denyReadLastSubpaths.map((p) => subpath(p.replace(/\/+$/, "")))
52663
+ ];
52664
+ lines.push(`(deny file-read* ${lastReadDenies.join(" ")})`);
52665
+ const denyWrites = [
52666
+ ...SENSITIVE_WRITE_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
52667
+ ...SENSITIVE_WRITE_LITERALS.map((rel) => literal2(`${home}/${rel}`)),
52668
+ // Caller-supplied write-denies (e.g. `<baseRepo>/.git/hooks` + `.git/config`) last so
52669
+ // they override the allowWritePaths re-allow of the shared object store above, while
52670
+ // `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
52671
+ ...denyWriteSubpaths,
52672
+ ...denyWriteLiterals
52673
+ ];
52674
+ lines.push(`(deny file-write* ${denyWrites.join(" ")})`);
52675
+ lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
52676
+ if (!input.allowNetwork) {
52677
+ lines.push("(deny network*)");
52678
+ } else if (input.strict) {
52679
+ lines.push("(allow network*)");
52680
+ }
52681
+ return `${lines.join("\n")}
52682
+ `;
52683
+ }
52684
+ function canonical(p) {
52685
+ try {
52686
+ return realpathSync(p);
52687
+ } catch {
52688
+ return p;
52689
+ }
52690
+ }
52691
+ function canonicalFile(p) {
52692
+ return path3.join(canonical(path3.dirname(p)), path3.basename(p));
52693
+ }
52694
+ function isSandboxExecAvailable() {
52695
+ try {
52696
+ return existsSync2(SANDBOX_EXEC_PATH);
52697
+ } catch {
52698
+ return false;
52699
+ }
52700
+ }
52701
+ function resolveSandboxSpec(options) {
52702
+ if (options.mode === "off") {
52703
+ return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
52704
+ }
52705
+ if (options.platform !== "darwin") {
52706
+ return { kind: "none", reason: `no OS sandbox on ${options.platform} (seatbelt is macOS-only; Linux confinement is a follow-up)` };
52707
+ }
52708
+ const available = options.sandboxExecAvailable ?? isSandboxExecAvailable();
52709
+ if (!available) {
52710
+ return { kind: "none", reason: `sandbox-exec not found at ${SANDBOX_EXEC_PATH}` };
52711
+ }
52712
+ const workspaceDir = canonical(options.workspaceDir);
52713
+ const tmpDir = canonical(options.tmpDir);
52714
+ const homeDir = canonical(options.homeDir);
52715
+ const allowNetwork = options.allowNetwork ?? true;
52716
+ const denyReadPaths = [canonicalFile(options.stateFile)];
52717
+ if (options.runnerBaseDir) {
52718
+ denyReadPaths.push(`${canonical(options.runnerBaseDir).replace(/\/+$/, "")}/`);
52719
+ }
52720
+ const denyWritePaths = (options.denyWritePaths ?? []).map(
52721
+ (p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
52722
+ );
52723
+ denyWritePaths.push(`${canonicalFile(RUNNER_ASKPASS_DIR)}/`);
52724
+ const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
52725
+ const denyReadLastSubpaths = [`${canonicalFile(RUNNER_ASKPASS_DIR)}/`];
52726
+ const profile = buildSeatbeltProfile({
52727
+ workspaceDir,
52728
+ tmpDir,
52729
+ homeDir,
52730
+ denyReadPaths,
52731
+ denyReadLastSubpaths,
52732
+ allowWritePaths,
52733
+ denyWritePaths,
52734
+ allowNetwork,
52735
+ strict: options.mode === "strict"
52736
+ });
52737
+ return {
52738
+ kind: "seatbelt",
52739
+ reason: options.mode === "strict" ? "seatbelt (strict: deny-default reads + write confinement)" : "seatbelt (guard: deny sensitive reads/writes)",
52740
+ profile,
52741
+ allowNetwork
52742
+ };
52743
+ }
52744
+ function forgeBuildUnsupportedReason(platform2) {
52745
+ if (platform2 === "darwin") {
52746
+ return null;
52747
+ }
52748
+ return `Forge build execution is unsupported on this host: no macOS seatbelt (sandbox-exec) confinement is available on '${platform2}', so this runner cannot prove filesystem/egress isolation for an untrusted build turn. The server will not offer Forge-phase work to this runner \u2014 unless its tenant is on the narrow, operator-established trust allowlist (FORGE_SANDBOX_TRUSTED_TENANT_IDS) \u2014 until real Linux confinement (namespaces/Landlock, tracked as a DER-1343 follow-up) ships.`;
52749
+ }
52750
+ function wrapCommandWithSandbox(spec, command, args, profilePath) {
52751
+ if (spec.kind === "none" || profilePath === null) {
52752
+ return { command, args, profilePath: null };
52753
+ }
52754
+ return {
52755
+ command: SANDBOX_EXEC_PATH,
52756
+ args: ["-f", profilePath, command, ...args],
52757
+ profilePath
52758
+ };
52759
+ }
52760
+ async function sweepStaleRunnerTempFiles(dir, options = {}) {
52761
+ const maxAgeMs = options.maxAgeMs ?? RUNNER_TEMP_STARTUP_SWEEP_MAX_AGE_MS;
52762
+ const now = options.now ?? Date.now();
52763
+ let entries;
52764
+ try {
52765
+ entries = await readdir(dir);
52766
+ } catch {
52767
+ return [];
52768
+ }
52769
+ const removed = [];
52770
+ await Promise.all(
52771
+ entries.map(async (name) => {
52772
+ if (!RUNNER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix))) {
52773
+ return;
52774
+ }
52775
+ const full = path3.join(dir, name);
52776
+ try {
52777
+ const info = await stat2(full);
52778
+ if (now - info.mtimeMs < maxAgeMs) {
52779
+ return;
52780
+ }
52781
+ await rm2(full, { recursive: true, force: true });
52782
+ removed.push(name);
52783
+ } catch {
52784
+ }
52785
+ })
52786
+ );
52787
+ return removed;
52788
+ }
52789
+
52205
52790
  // src/runner-service.ts
52206
52791
  import { execFile as execFileCallback2 } from "node:child_process";
52207
- import { realpathSync } from "node:fs";
52208
- import { access, mkdir as mkdir2, readFile as readFile3, readdir, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
52792
+ import { realpathSync as realpathSync2 } from "node:fs";
52793
+ import { access, mkdir as mkdir2, readFile as readFile3, readdir as readdir2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
52209
52794
  import { homedir as homedir2 } from "node:os";
52210
- import path3 from "node:path";
52795
+ import path4 from "node:path";
52211
52796
  import { promisify as promisify2 } from "node:util";
52212
52797
  var execFile2 = promisify2(execFileCallback2);
52213
52798
  var SERVICE_VERBS = ["install-service", "start", "stop", "restart", "status", "logs", "uninstall"];
@@ -52224,9 +52809,9 @@ function isRunnerServiceInvocation(args) {
52224
52809
  function runnerServicePaths(homeDir, runnerName) {
52225
52810
  const safeName = safeRunnerName(runnerName);
52226
52811
  return {
52227
- launchAgentPath: path3.join(homeDir, "Library", "LaunchAgents", `${serviceLabel(safeName)}.plist`),
52228
- stateFile: path3.join(homeDir, "Library", "Application Support", cliBrand.name, `runner-${safeName}.json`),
52229
- logDir: path3.join(homeDir, "Library", "Logs", cliBrand.name, "runner", safeName)
52812
+ launchAgentPath: path4.join(homeDir, "Library", "LaunchAgents", `${serviceLabel(safeName)}.plist`),
52813
+ stateFile: path4.join(homeDir, "Library", "Application Support", cliBrand.name, `runner-${safeName}.json`),
52814
+ logDir: path4.join(homeDir, "Library", "Logs", cliBrand.name, "runner", safeName)
52230
52815
  };
52231
52816
  }
52232
52817
  function renderLaunchdPlist(config2) {
@@ -52268,9 +52853,9 @@ ${config2.allowDevelopmentAppUrl === true ? ` <key>${allowInsecureAppUrlEnv}<
52268
52853
  <key>RunAtLoad</key>
52269
52854
  <true/>
52270
52855
  <key>StandardOutPath</key>
52271
- <string>${escapePlist(path3.join(config2.logDir, "stdout.log"))}</string>
52856
+ <string>${escapePlist(path4.join(config2.logDir, "stdout.log"))}</string>
52272
52857
  <key>StandardErrorPath</key>
52273
- <string>${escapePlist(path3.join(config2.logDir, "stderr.log"))}</string>
52858
+ <string>${escapePlist(path4.join(config2.logDir, "stderr.log"))}</string>
52274
52859
  </dict>
52275
52860
  </plist>
52276
52861
  `;
@@ -52338,8 +52923,8 @@ async function installService(options) {
52338
52923
  const paths = runnerServicePaths(options.homeDir, safeName);
52339
52924
  const label = serviceLabel(safeName);
52340
52925
  await mkdir2(paths.logDir, { recursive: true });
52341
- await mkdir2(path3.dirname(paths.stateFile), { recursive: true });
52342
- await mkdir2(path3.dirname(paths.launchAgentPath), { recursive: true });
52926
+ await mkdir2(path4.dirname(paths.stateFile), { recursive: true });
52927
+ await mkdir2(path4.dirname(paths.launchAgentPath), { recursive: true });
52343
52928
  if (options.userCode !== void 0) {
52344
52929
  const paired = await pairRunnerOnce(options, safeName, paths.stateFile, options.userCode);
52345
52930
  if (!paired) {
@@ -52494,8 +53079,8 @@ async function logsService(options) {
52494
53079
  const safeName = safeRunnerName(options.runnerName);
52495
53080
  const paths = runnerServicePaths(options.homeDir, safeName);
52496
53081
  const label = serviceLabel(safeName);
52497
- const stdoutLog = path3.join(paths.logDir, "stdout.log");
52498
- const stderrLog = path3.join(paths.logDir, "stderr.log");
53082
+ const stdoutLog = path4.join(paths.logDir, "stdout.log");
53083
+ const stderrLog = path4.join(paths.logDir, "stderr.log");
52499
53084
  options.io.stdout.write(
52500
53085
  `${[
52501
53086
  `runner service logs for ${label}:`,
@@ -52526,7 +53111,7 @@ async function uninstallService(options) {
52526
53111
  await bootoutQuietly(options.exec, options.uid, paths.launchAgentPath);
52527
53112
  let removed = false;
52528
53113
  try {
52529
- await rm2(paths.launchAgentPath, { force: true });
53114
+ await rm3(paths.launchAgentPath, { force: true });
52530
53115
  removed = true;
52531
53116
  } catch (error51) {
52532
53117
  options.io.stderr.write(
@@ -52599,10 +53184,10 @@ function serviceTargets(uid, label) {
52599
53184
  }
52600
53185
  async function inspectRunnerServiceInstallations(options = {}) {
52601
53186
  const homeDir = options.homeDir ?? homedir2();
52602
- const launchAgentsDir = path3.join(homeDir, "Library", "LaunchAgents");
53187
+ const launchAgentsDir = path4.join(homeDir, "Library", "LaunchAgents");
52603
53188
  let names = [];
52604
53189
  try {
52605
- const entries = await readdir(launchAgentsDir);
53190
+ const entries = await readdir2(launchAgentsDir);
52606
53191
  names = entries.map((entry) => runnerNameFromPlist(entry)).filter((value) => value !== null).sort((left, right) => left.localeCompare(right));
52607
53192
  } catch {
52608
53193
  return [];
@@ -52728,11 +53313,11 @@ function resolveNodePath() {
52728
53313
  }
52729
53314
  function buildServicePath(nodePath, homeDir) {
52730
53315
  const entries = [
52731
- path3.dirname(nodePath),
53316
+ path4.dirname(nodePath),
52732
53317
  "/opt/homebrew/bin",
52733
53318
  "/usr/local/bin",
52734
- path3.join(homeDir, ".local", "bin"),
52735
- path3.join(homeDir, "bin"),
53319
+ path4.join(homeDir, ".local", "bin"),
53320
+ path4.join(homeDir, "bin"),
52736
53321
  "/usr/bin",
52737
53322
  "/bin",
52738
53323
  "/usr/sbin",
@@ -52781,7 +53366,7 @@ function resolveBinPath() {
52781
53366
  return cliBrand.binName;
52782
53367
  }
52783
53368
  try {
52784
- return realpathSync(entry);
53369
+ return realpathSync2(entry);
52785
53370
  } catch {
52786
53371
  return entry;
52787
53372
  }
@@ -52920,6 +53505,10 @@ async function runDoctor(options) {
52920
53505
  lines.push(`version: ${currentVersion ?? "unknown"}`);
52921
53506
  const pathStatus = await checkPath(env);
52922
53507
  lines.push(pathStatus);
53508
+ const forgeUnsupportedReason = forgeBuildUnsupportedReason(process.platform);
53509
+ if (forgeUnsupportedReason) {
53510
+ lines.push(`forge_build: unsupported - ${forgeUnsupportedReason}`);
53511
+ }
52923
53512
  try {
52924
53513
  const session = await store.read();
52925
53514
  lines.push(`session: ${session ? `present (${store.describe()})` : `missing - run ${cliBrand.binName} login --device`}`);
@@ -53039,9 +53628,8 @@ import { promisify as promisify5 } from "node:util";
53039
53628
  // src/forge-workspace.ts
53040
53629
  import { execFile as execFileCallback4 } from "node:child_process";
53041
53630
  import { createHash } from "node:crypto";
53042
- import { chmod, mkdir as mkdir3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
53043
- import { tmpdir } from "node:os";
53044
- import path4 from "node:path";
53631
+ import { chmod, lstat, mkdir as mkdir3, mkdtemp, realpath, rm as rm4, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
53632
+ import path5 from "node:path";
53045
53633
  import { setTimeout as sleep2 } from "node:timers/promises";
53046
53634
  import { promisify as promisify4 } from "node:util";
53047
53635
 
@@ -53178,20 +53766,20 @@ function shortRequestId(buildRequestId) {
53178
53766
  return slice.length > 0 ? slice : createHash("sha1").update(buildRequestId).digest("hex").slice(0, 8);
53179
53767
  }
53180
53768
  function requestRoot(baseDir, buildRequestId) {
53181
- return path4.join(baseDir, "work", shortRequestId(buildRequestId));
53769
+ return path5.join(baseDir, "work", shortRequestId(buildRequestId));
53182
53770
  }
53183
53771
  function baseRepoDir(baseDir, buildRequestId) {
53184
- return path4.join(requestRoot(baseDir, buildRequestId), "base");
53772
+ return path5.join(requestRoot(baseDir, buildRequestId), "base");
53185
53773
  }
53186
53774
  function branchName(buildRequestId, changeset) {
53187
53775
  const short = shortRequestId(buildRequestId);
53188
53776
  return changeset.total > 1 ? `forge/${short}-cs${changeset.ordinal}` : `forge/${short}`;
53189
53777
  }
53190
53778
  function buildWorktreeDir(baseDir, buildRequestId, ordinal) {
53191
- return path4.join(requestRoot(baseDir, buildRequestId), `cs${ordinal}`);
53779
+ return path5.join(requestRoot(baseDir, buildRequestId), `cs${ordinal}`);
53192
53780
  }
53193
53781
  function reviewWorktreeDir(baseDir, buildRequestId, ordinal) {
53194
- return path4.join(requestRoot(baseDir, buildRequestId), `review-cs${ordinal}`);
53782
+ return path5.join(requestRoot(baseDir, buildRequestId), `review-cs${ordinal}`);
53195
53783
  }
53196
53784
  function remoteUrlFor(options) {
53197
53785
  if (options.remoteUrl) {
@@ -53201,14 +53789,14 @@ function remoteUrlFor(options) {
53201
53789
  }
53202
53790
  async function pathExists(target) {
53203
53791
  try {
53204
- await stat2(target);
53792
+ await stat3(target);
53205
53793
  return true;
53206
53794
  } catch {
53207
53795
  return false;
53208
53796
  }
53209
53797
  }
53210
53798
  async function isGitRepo(dir) {
53211
- return await pathExists(path4.join(dir, ".git")) || await pathExists(path4.join(dir, "HEAD"));
53799
+ return await pathExists(path5.join(dir, ".git")) || await pathExists(path5.join(dir, "HEAD"));
53212
53800
  }
53213
53801
  async function runGit(args, cwd, timeoutMs = LOCAL_GIT_TIMEOUT_MS) {
53214
53802
  try {
@@ -53260,13 +53848,31 @@ async function withNetworkRetries(op, fn, retryCounter) {
53260
53848
  }
53261
53849
  throw new Error(`${op} exhausted retries: ${lastError instanceof Error ? redactSecrets(lastError.message) : redactSecrets(String(lastError))}`);
53262
53850
  }
53263
- async function openAskpassSession(credential) {
53264
- const helperPath = path4.join(
53265
- // Same temp root the runner uses; the `rost-runner-askpass-` prefix is on the
53266
- // startup sweep's reap list, so a killed turn's orphan is cleaned up.
53267
- tmpdir(),
53268
- `rost-runner-askpass-${createHash("sha256").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.sh`
53269
- );
53851
+ async function canonicalAskpassBase(baseDir) {
53852
+ return path5.join(await realpath(path5.dirname(baseDir)), path5.basename(baseDir));
53853
+ }
53854
+ async function prepareAskpassTurnDir(baseDir) {
53855
+ await mkdir3(baseDir, { recursive: true, mode: 448 });
53856
+ const baseInfo = await lstat(baseDir);
53857
+ if (baseInfo.isSymbolicLink() || !baseInfo.isDirectory()) {
53858
+ throw new Error(`refusing to use askpass dir ${baseDir}: not a real directory (possible symlink attack)`);
53859
+ }
53860
+ if (typeof process.getuid === "function" && baseInfo.uid !== process.getuid()) {
53861
+ throw new Error(`refusing to use askpass dir ${baseDir}: not owned by this user (possible pre-planted dir)`);
53862
+ }
53863
+ await chmod(baseDir, 448);
53864
+ const turnDir = await mkdtemp(path5.join(baseDir, "rost-runner-askpass-"));
53865
+ const canonicalBase = await canonicalAskpassBase(baseDir);
53866
+ const realTurn = await realpath(turnDir);
53867
+ if (path5.dirname(realTurn) !== canonicalBase) {
53868
+ await rm4(turnDir, { recursive: true, force: true });
53869
+ throw new Error(`refusing to use askpass dir ${turnDir}: resolved outside ${canonicalBase} (possible symlink attack)`);
53870
+ }
53871
+ return turnDir;
53872
+ }
53873
+ async function openAskpassSession(credential, baseDir = RUNNER_ASKPASS_DIR) {
53874
+ const turnDir = await prepareAskpassTurnDir(baseDir);
53875
+ const helperPath = path5.join(turnDir, "askpass.sh");
53270
53876
  const helper = [
53271
53877
  "#!/bin/sh",
53272
53878
  "# One-shot Forge GIT_ASKPASS helper. Contains no secret; reads the token from env.",
@@ -53289,7 +53895,7 @@ async function openAskpassSession(credential) {
53289
53895
  return {
53290
53896
  env,
53291
53897
  cleanup: async () => {
53292
- await rm3(helperPath, { force: true });
53898
+ await rm4(turnDir, { recursive: true, force: true });
53293
53899
  }
53294
53900
  };
53295
53901
  }
@@ -53327,8 +53933,8 @@ async function localBranchExists(baseRepo, branch) {
53327
53933
  async function worktreeRegistered(baseRepo, worktreeDir) {
53328
53934
  try {
53329
53935
  const listing = await runGit(["worktree", "list", "--porcelain"], baseRepo);
53330
- const target = path4.resolve(worktreeDir);
53331
- return listing.split("\n").some((line) => line.startsWith("worktree ") && path4.resolve(line.slice("worktree ".length).trim()) === target);
53936
+ const target = path5.resolve(worktreeDir);
53937
+ return listing.split("\n").some((line) => line.startsWith("worktree ") && path5.resolve(line.slice("worktree ".length).trim()) === target);
53332
53938
  } catch {
53333
53939
  return false;
53334
53940
  }
@@ -53339,7 +53945,7 @@ async function configureForgeIdentity(baseRepo) {
53339
53945
  }
53340
53946
  async function ensureBaseClone(input) {
53341
53947
  const baseRepo = baseRepoDir(input.baseDir, input.buildRequestId);
53342
- await mkdir3(path4.dirname(baseRepo), { recursive: true, mode: 448 });
53948
+ await mkdir3(path5.dirname(baseRepo), { recursive: true, mode: 448 });
53343
53949
  await withLedgerStep(input.ledger, "clone", (retryCounter) => withAskpass(input.credential, async (netEnv) => {
53344
53950
  if (await isGitRepo(baseRepo)) {
53345
53951
  await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", input.url, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
@@ -53388,7 +53994,7 @@ async function prepareMergeWorkspace(options) {
53388
53994
  ledger: options.ledger
53389
53995
  });
53390
53996
  const branch = options.headBranch;
53391
- const worktreeDir = path4.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
53997
+ const worktreeDir = path5.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
53392
53998
  const originDefault = `origin/${options.repo.default_branch}`;
53393
53999
  const originHead = `origin/${branch}`;
53394
54000
  await withLedgerStep(options.ledger, "branch", async () => {
@@ -53425,7 +54031,7 @@ async function prepareReviewWorktree(options, baseRepo) {
53425
54031
  if (await worktreeRegistered(baseRepo, worktreeDir)) {
53426
54032
  await removeWorktree(baseRepo, worktreeDir);
53427
54033
  } else if (await pathExists(worktreeDir)) {
53428
- await rm3(worktreeDir, { recursive: true, force: true });
54034
+ await rm4(worktreeDir, { recursive: true, force: true });
53429
54035
  await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
53430
54036
  }
53431
54037
  await runGit(["worktree", "add", "--detach", worktreeDir, headSha], baseRepo);
@@ -53758,267 +54364,18 @@ async function removeWorktree(baseRepo, worktreeDir) {
53758
54364
  try {
53759
54365
  await runGit(["worktree", "remove", "--force", worktreeDir], baseRepo);
53760
54366
  } catch {
53761
- await rm3(worktreeDir, { recursive: true, force: true });
54367
+ await rm4(worktreeDir, { recursive: true, force: true });
53762
54368
  }
53763
54369
  await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
53764
54370
  }
53765
54371
  async function removeWorkspace(ws) {
53766
54372
  if (!await pathExists(ws.baseRepo)) {
53767
- await rm3(ws.worktreeDir, { recursive: true, force: true });
54373
+ await rm4(ws.worktreeDir, { recursive: true, force: true });
53768
54374
  return;
53769
54375
  }
53770
54376
  await removeWorktree(ws.baseRepo, ws.worktreeDir);
53771
54377
  }
53772
54378
 
53773
- // src/runner-sandbox.ts
53774
- import { existsSync as existsSync2, realpathSync as realpathSync2 } from "node:fs";
53775
- import { readdir as readdir2, rm as rm4, stat as stat3 } from "node:fs/promises";
53776
- import path5 from "node:path";
53777
- var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
53778
- var RUNNER_TEMP_PREFIXES = [
53779
- "rost-runner-mcp-",
53780
- "rost-runner-sandbox-",
53781
- "rost-runner-askpass-"
53782
- ];
53783
- var SENSITIVE_READ_SUBPATHS = [
53784
- ".ssh",
53785
- ".aws",
53786
- ".gnupg",
53787
- ".config/gcloud",
53788
- ".config/gh",
53789
- ".kube",
53790
- ".docker",
53791
- ".npmrc"
53792
- ];
53793
- var SENSITIVE_READ_LITERALS = [".netrc"];
53794
- var SENSITIVE_WRITE_SUBPATHS = [
53795
- ".ssh",
53796
- ".aws",
53797
- ".gnupg",
53798
- ".config/git",
53799
- "bin",
53800
- "Library/LaunchAgents"
53801
- ];
53802
- var SENSITIVE_WRITE_LITERALS = [
53803
- ".gitconfig",
53804
- ".zshrc",
53805
- ".zprofile",
53806
- ".zshenv",
53807
- ".zlogin",
53808
- ".bashrc",
53809
- ".bash_profile",
53810
- ".bash_login",
53811
- ".profile"
53812
- ];
53813
- function parseSandboxMode(value) {
53814
- const normalized = (value ?? "auto").trim().toLowerCase();
53815
- if (normalized === "off" || normalized === "strict" || normalized === "auto" || normalized === "") {
53816
- return normalized === "" ? "auto" : normalized;
53817
- }
53818
- return "auto";
53819
- }
53820
- function parseSandboxNetwork(value) {
53821
- const normalized = (value ?? "").trim().toLowerCase();
53822
- if (normalized === "" || normalized === "allow") {
53823
- return { allowNetwork: true, unrecognized: null };
53824
- }
53825
- if (normalized === "deny") {
53826
- return { allowNetwork: false, unrecognized: null };
53827
- }
53828
- return { allowNetwork: true, unrecognized: value ?? "" };
53829
- }
53830
- function sbplString(value) {
53831
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
53832
- }
53833
- function subpath(value) {
53834
- return `(subpath ${sbplString(value)})`;
53835
- }
53836
- function literal2(value) {
53837
- return `(literal ${sbplString(value)})`;
53838
- }
53839
- var CLAUDE_TEMP_SBPL_REGEX = '(regex #"^/private/tmp/claude-[^/]+")';
53840
- var STRICT_SYSTEM_READ_SUBPATHS = [
53841
- "/usr",
53842
- "/bin",
53843
- "/sbin",
53844
- "/System",
53845
- "/Library",
53846
- "/private/etc",
53847
- "/private/var/db",
53848
- "/dev",
53849
- "/opt",
53850
- "/nix"
53851
- ];
53852
- function buildSeatbeltProfile(input) {
53853
- const home = input.homeDir.replace(/\/+$/, "");
53854
- const lines = ["(version 1)"];
53855
- const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
53856
- const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
53857
- const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
53858
- const denyWriteLiterals = input.denyWritePaths.filter((p) => !p.endsWith("/")).map(literal2);
53859
- const credentialReadDenies = [
53860
- ...SENSITIVE_READ_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
53861
- ...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
53862
- ];
53863
- if (input.strict) {
53864
- lines.push("(deny default)");
53865
- lines.push("(allow process*)");
53866
- lines.push("(allow sysctl-read)");
53867
- lines.push("(allow mach-lookup)");
53868
- lines.push("(allow iokit-open)");
53869
- lines.push("(allow system-socket)");
53870
- lines.push("(allow signal (target self))");
53871
- lines.push("(allow file-read-metadata)");
53872
- const strictReads = [
53873
- ...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
53874
- subpath(`${home}/.claude`),
53875
- subpath(`${home}/.codex`),
53876
- subpath(input.tmpDir)
53877
- ];
53878
- lines.push(`(allow file-read* ${strictReads.join(" ")})`);
53879
- } else {
53880
- lines.push("(allow default)");
53881
- }
53882
- lines.push("(deny appleevent-send)");
53883
- if (denyReadBaseSubpaths.length > 0) {
53884
- lines.push(`(deny file-read* ${denyReadBaseSubpaths.join(" ")})`);
53885
- }
53886
- const workspaceReads = [subpath(input.workspaceDir), ...input.allowWritePaths.map(subpath), CLAUDE_TEMP_SBPL_REGEX];
53887
- lines.push(`(allow file-read* ${workspaceReads.join(" ")})`);
53888
- lines.push("(deny file-write*)");
53889
- const allowWrites = [
53890
- subpath(input.workspaceDir),
53891
- subpath(input.tmpDir),
53892
- subpath(`${home}/.claude`),
53893
- subpath(`${home}/.codex`),
53894
- ...input.allowWritePaths.map(subpath),
53895
- CLAUDE_TEMP_SBPL_REGEX,
53896
- // DER-1398: Claude Code's per-session + per-command temp dirs.
53897
- subpath("/dev/fd"),
53898
- literal2("/dev/null"),
53899
- literal2("/dev/stdout"),
53900
- literal2("/dev/stderr"),
53901
- literal2("/dev/tty"),
53902
- literal2("/dev/dtracehelper")
53903
- ];
53904
- lines.push(`(allow file-write* ${allowWrites.join(" ")})`);
53905
- lines.push(`(deny file-read* ${[...credentialReadDenies, ...denyReadLiterals].join(" ")})`);
53906
- const denyWrites = [
53907
- ...SENSITIVE_WRITE_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
53908
- ...SENSITIVE_WRITE_LITERALS.map((rel) => literal2(`${home}/${rel}`)),
53909
- // Caller-supplied write-denies (e.g. `<baseRepo>/.git/hooks` + `.git/config`) last so
53910
- // they override the allowWritePaths re-allow of the shared object store above, while
53911
- // `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
53912
- ...denyWriteSubpaths,
53913
- ...denyWriteLiterals
53914
- ];
53915
- lines.push(`(deny file-write* ${denyWrites.join(" ")})`);
53916
- lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
53917
- if (!input.allowNetwork) {
53918
- lines.push("(deny network*)");
53919
- } else if (input.strict) {
53920
- lines.push("(allow network*)");
53921
- }
53922
- return `${lines.join("\n")}
53923
- `;
53924
- }
53925
- function canonical(p) {
53926
- try {
53927
- return realpathSync2(p);
53928
- } catch {
53929
- return p;
53930
- }
53931
- }
53932
- function canonicalFile(p) {
53933
- return path5.join(canonical(path5.dirname(p)), path5.basename(p));
53934
- }
53935
- function isSandboxExecAvailable() {
53936
- try {
53937
- return existsSync2(SANDBOX_EXEC_PATH);
53938
- } catch {
53939
- return false;
53940
- }
53941
- }
53942
- function resolveSandboxSpec(options) {
53943
- if (options.mode === "off") {
53944
- return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
53945
- }
53946
- if (options.platform !== "darwin") {
53947
- return { kind: "none", reason: `no OS sandbox on ${options.platform} (seatbelt is macOS-only; Linux confinement is a follow-up)` };
53948
- }
53949
- const available = options.sandboxExecAvailable ?? isSandboxExecAvailable();
53950
- if (!available) {
53951
- return { kind: "none", reason: `sandbox-exec not found at ${SANDBOX_EXEC_PATH}` };
53952
- }
53953
- const workspaceDir = canonical(options.workspaceDir);
53954
- const tmpDir = canonical(options.tmpDir);
53955
- const homeDir = canonical(options.homeDir);
53956
- const allowNetwork = options.allowNetwork ?? true;
53957
- const denyReadPaths = [canonicalFile(options.stateFile)];
53958
- if (options.runnerBaseDir) {
53959
- denyReadPaths.push(`${canonical(options.runnerBaseDir).replace(/\/+$/, "")}/`);
53960
- }
53961
- const denyWritePaths = (options.denyWritePaths ?? []).map(
53962
- (p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
53963
- );
53964
- const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
53965
- const profile = buildSeatbeltProfile({
53966
- workspaceDir,
53967
- tmpDir,
53968
- homeDir,
53969
- denyReadPaths,
53970
- allowWritePaths,
53971
- denyWritePaths,
53972
- allowNetwork,
53973
- strict: options.mode === "strict"
53974
- });
53975
- return {
53976
- kind: "seatbelt",
53977
- reason: options.mode === "strict" ? "seatbelt (strict: deny-default reads + write confinement)" : "seatbelt (guard: deny sensitive reads/writes)",
53978
- profile,
53979
- allowNetwork
53980
- };
53981
- }
53982
- function wrapCommandWithSandbox(spec, command, args, profilePath) {
53983
- if (spec.kind === "none" || profilePath === null) {
53984
- return { command, args, profilePath: null };
53985
- }
53986
- return {
53987
- command: SANDBOX_EXEC_PATH,
53988
- args: ["-f", profilePath, command, ...args],
53989
- profilePath
53990
- };
53991
- }
53992
- async function sweepStaleRunnerTempFiles(dir, options = {}) {
53993
- const maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1e3;
53994
- const now = options.now ?? Date.now();
53995
- let entries;
53996
- try {
53997
- entries = await readdir2(dir);
53998
- } catch {
53999
- return [];
54000
- }
54001
- const removed = [];
54002
- await Promise.all(
54003
- entries.map(async (name) => {
54004
- if (!RUNNER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix))) {
54005
- return;
54006
- }
54007
- const full = path5.join(dir, name);
54008
- try {
54009
- const info = await stat3(full);
54010
- if (now - info.mtimeMs < maxAgeMs) {
54011
- return;
54012
- }
54013
- await rm4(full, { force: true });
54014
- removed.push(name);
54015
- } catch {
54016
- }
54017
- })
54018
- );
54019
- return removed;
54020
- }
54021
-
54022
54379
  // src/runner-serve.ts
54023
54380
  var execFile5 = promisify5(execFileCallback5);
54024
54381
  var RUNNER_WORK_ORDER_TOOLS = [
@@ -54034,7 +54391,6 @@ var RUNNER_AICOS_TURN_TOOLS = [
54034
54391
  var CLAUDE_WORKSPACE_WRITE_LOCAL_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
54035
54392
  var CLAUDE_READ_LOCAL_TOOLS = ["Read", "Glob", "Grep"];
54036
54393
  var READ_STUB_TIMEOUT_MS = 18e4;
54037
- var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
54038
54394
  function claudeMcpToolName(name) {
54039
54395
  return name.startsWith("mcp__") ? name : `mcp__rost__${name}`;
54040
54396
  }
@@ -54062,12 +54418,19 @@ ${usage()}
54062
54418
  `);
54063
54419
  if (config2.sandboxNetworkWarning) {
54064
54420
  options.io.stderr.write(`warning: ${config2.sandboxNetworkWarning}
54421
+ `);
54422
+ }
54423
+ const forgeUnsupportedReason = forgeBuildUnsupportedReason(process.platform);
54424
+ if (forgeUnsupportedReason) {
54425
+ options.io.stdout.write(`forge build: ${forgeUnsupportedReason}
54065
54426
  `);
54066
54427
  }
54067
54428
  try {
54068
54429
  const swept = await sweepStaleRunnerTempFiles(tmpdir2());
54069
- if (swept.length > 0) {
54070
- options.io.stdout.write(`swept ${swept.length} stale runner temp file(s)
54430
+ const sweptAskpass = await sweepStaleRunnerTempFiles(RUNNER_ASKPASS_DIR);
54431
+ const total = swept.length + sweptAskpass.length;
54432
+ if (total > 0) {
54433
+ options.io.stdout.write(`swept ${total} stale runner temp file(s)
54071
54434
  `);
54072
54435
  }
54073
54436
  } catch {
@@ -54104,9 +54467,19 @@ ${usage()}
54104
54467
  }
54105
54468
  if (localRuntime) {
54106
54469
  activeSessions += 1;
54470
+ const stopTurnHeartbeat = startTurnHeartbeat({
54471
+ fetchImpl,
54472
+ appUrl: options.appUrl,
54473
+ secret: state.runner_secret,
54474
+ intervalMs: config2.heartbeatMs,
54475
+ capabilities,
54476
+ io: options.io,
54477
+ telemetry: () => detectTelemetry(capabilities, env, activeSessions, cliVersion)
54478
+ });
54107
54479
  try {
54108
54480
  await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
54109
54481
  } finally {
54482
+ stopTurnHeartbeat();
54110
54483
  activeSessions = Math.max(0, activeSessions - 1);
54111
54484
  }
54112
54485
  }
@@ -54138,6 +54511,44 @@ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runt
54138
54511
  }
54139
54512
  }
54140
54513
  }
54514
+ function startTurnHeartbeat(deps) {
54515
+ let stopped = false;
54516
+ let inFlight = false;
54517
+ const timer = setInterval(() => {
54518
+ if (stopped || inFlight) {
54519
+ return;
54520
+ }
54521
+ inFlight = true;
54522
+ const controller = new AbortController();
54523
+ const timeout = setTimeout(() => controller.abort(), deps.intervalMs);
54524
+ timeout.unref?.();
54525
+ void (async () => {
54526
+ try {
54527
+ const response = await post2(deps.fetchImpl, deps.appUrl, "/api/runner/heartbeat", {
54528
+ capabilities: deps.capabilities,
54529
+ telemetry: deps.telemetry()
54530
+ }, deps.secret, controller.signal);
54531
+ if (!stopped && response.status !== 200) {
54532
+ deps.io.stderr.write(`turn heartbeat ${response.status}: ${redactForLog(JSON.stringify(response.json))}
54533
+ `);
54534
+ }
54535
+ } catch (error51) {
54536
+ if (!stopped) {
54537
+ deps.io.stderr.write(`turn heartbeat error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
54538
+ `);
54539
+ }
54540
+ } finally {
54541
+ clearTimeout(timeout);
54542
+ inFlight = false;
54543
+ }
54544
+ })();
54545
+ }, deps.intervalMs);
54546
+ timer.unref?.();
54547
+ return () => {
54548
+ stopped = true;
54549
+ clearInterval(timer);
54550
+ };
54551
+ }
54141
54552
  function resolveAccountAlias(env) {
54142
54553
  return (env.RUNNER_MODEL_ACCOUNT_ALIAS ?? env.ROST_RUNNER_MODEL_ACCOUNT_ALIAS ?? "").trim() || "default";
54143
54554
  }
@@ -54518,7 +54929,7 @@ async function probeForgeDiskFreeMb(homeDir) {
54518
54929
  return null;
54519
54930
  }
54520
54931
  }
54521
- async function post2(fetchImpl, appUrl2, pathName, body, token) {
54932
+ async function post2(fetchImpl, appUrl2, pathName, body, token, signal) {
54522
54933
  const headers = { "content-type": "application/json" };
54523
54934
  if (token) {
54524
54935
  headers.authorization = `Bearer ${token}`;
@@ -54526,7 +54937,10 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
54526
54937
  const response = await fetchImpl(`${appUrl2.replace(/\/+$/, "")}${pathName}`, {
54527
54938
  method: "POST",
54528
54939
  headers,
54529
- body: JSON.stringify(body)
54940
+ body: JSON.stringify(body),
54941
+ // Optional per-call abort. Callers that pass no signal are unchanged; only the turn
54942
+ // keepalive uses it to bound a hung POST so its in-flight guard always releases (DER-1484).
54943
+ ...signal ? { signal } : {}
54530
54944
  });
54531
54945
  const text = await response.text();
54532
54946
  let json2;
@@ -55003,6 +55417,13 @@ function buildTurnPrompt(input) {
55003
55417
  `Claimed ${kind === "turn_execution" ? "turn execution" : "work order"} context: ${JSON.stringify(context).slice(0, 8e3)}`
55004
55418
  ].filter((line) => line.length > 0).join("\n");
55005
55419
  }
55420
+ async function sweepOrphanedTurnTempFiles() {
55421
+ try {
55422
+ await sweepStaleRunnerTempFiles(tmpdir2(), { maxAgeMs: RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS });
55423
+ await sweepStaleRunnerTempFiles(RUNNER_ASKPASS_DIR, { maxAgeMs: RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS });
55424
+ } catch {
55425
+ }
55426
+ }
55006
55427
  async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
55007
55428
  const execution = parseRunnerExecutionContract(workOrder);
55008
55429
  if (execution?.profile === "forge_merge") {
@@ -55085,6 +55506,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
55085
55506
  if (sandboxProfilePath) {
55086
55507
  void rm5(sandboxProfilePath, { force: true });
55087
55508
  }
55509
+ void sweepOrphanedTurnTempFiles();
55088
55510
  if (forgePrep) {
55089
55511
  if (plan.profile === "forge_build" && result.ok && forgePrep.credential.canPush) {
55090
55512
  result = await finalizeForgeBuildPush(ctx, forgePrep, result);
@@ -55276,6 +55698,7 @@ async function spawnMergeConflictModel(ctx, input) {
55276
55698
  if (sandboxProfilePath) {
55277
55699
  void rm5(sandboxProfilePath, { force: true });
55278
55700
  }
55701
+ void sweepOrphanedTurnTempFiles();
55279
55702
  return result;
55280
55703
  }
55281
55704
  async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
@@ -55373,6 +55796,20 @@ async function executeForgeMerge(ctx, input) {
55373
55796
  function scrubTranscriptSessionIds(value) {
55374
55797
  return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
55375
55798
  }
55799
+ function killProcessTree(child) {
55800
+ const pid = child.pid;
55801
+ if (typeof pid === "number") {
55802
+ try {
55803
+ process.kill(-pid, "SIGKILL");
55804
+ return;
55805
+ } catch {
55806
+ }
55807
+ }
55808
+ try {
55809
+ child.kill("SIGKILL");
55810
+ } catch {
55811
+ }
55812
+ }
55376
55813
  function runModelProcess(input) {
55377
55814
  return new Promise((resolve2) => {
55378
55815
  let settled = false;
@@ -55386,12 +55823,20 @@ function runModelProcess(input) {
55386
55823
  const child = spawn(input.command, input.args, {
55387
55824
  cwd: input.cwd,
55388
55825
  env: input.env,
55389
- stdio: ["ignore", "pipe", "pipe"]
55826
+ stdio: ["ignore", "pipe", "pipe"],
55827
+ // DER-1569 FIX 1: give the turn its OWN process group so a timeout SIGKILL reaps the
55828
+ // WHOLE tree, not just the direct child. sandbox-exec execvp's into the model (pid
55829
+ // preserved), and the model's Bash tool forks build/test grandchildren that inherit this
55830
+ // pgid; a plain `child.kill()` signals only the direct pid and orphans them to init.
55831
+ // `detached` makes the child a group leader (pgid == child.pid) so `process.kill(-pid)`
55832
+ // in killProcessTree hits the model and every descendant. We still await `close`, so the
55833
+ // reference is kept (no `unref()`); the parent lifecycle is unchanged.
55834
+ detached: true
55390
55835
  });
55391
55836
  let out = "";
55392
55837
  let err = "";
55393
55838
  const timer = setTimeout(() => {
55394
- child.kill("SIGKILL");
55839
+ killProcessTree(child);
55395
55840
  const partial2 = scrubTranscriptSessionIds(redactForLog((out || err || "").trim()));
55396
55841
  finish({
55397
55842
  ok: false,
@@ -55609,6 +56054,15 @@ function usage() {
55609
56054
  }
55610
56055
 
55611
56056
  // src/index.ts
56057
+ async function defaultConfirmPrompt(question) {
56058
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
56059
+ try {
56060
+ const answer = (await rl.question(`${question} `)).trim().toLowerCase();
56061
+ return answer === "y" || answer === "yes";
56062
+ } finally {
56063
+ rl.close();
56064
+ }
56065
+ }
55612
56066
  async function main(argv = process.argv.slice(2), options = {}) {
55613
56067
  const io = options.io ?? {
55614
56068
  stdout: process.stdout,
@@ -55617,7 +56071,10 @@ async function main(argv = process.argv.slice(2), options = {}) {
55617
56071
  // and stdout must be TTYs. A piped/headless/agent session fails this and so
55618
56072
  // will not auto-approve its own mint/revoke confirmation unless skip-
55619
56073
  // permissions is explicitly enabled.
55620
- interactive: process.stdin.isTTY === true && process.stdout.isTTY === true
56074
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
56075
+ // DER-1490: a real session prompts the human on stdin before approving a
56076
+ // pending confirmation gate.
56077
+ confirm: defaultConfirmPrompt
55621
56078
  };
55622
56079
  const login = options.login ?? loginWithBrowser;
55623
56080
  const deviceLogin = options.deviceLogin ?? loginWithDeviceCode;
@@ -56047,11 +56504,13 @@ async function executeMcp(io, client, appUrl2, args, env = process.env) {
56047
56504
  const options = parseMcpInstallOptions(args.slice(1));
56048
56505
  const envSkipPermissions = env[`${cliBrand.envPrefix}_CLI_SKIP_PERMISSIONS`] === "1";
56049
56506
  const canAutoApprove = io.interactive === true || options.skipPermissions || envSkipPermissions;
56507
+ const reviewed = io.interactive === true;
56050
56508
  const rendered = await renderMcpInstall({
56051
56509
  client,
56052
56510
  appUrl: appUrl2,
56053
56511
  options,
56054
- canAutoApprove
56512
+ canAutoApprove,
56513
+ reviewed
56055
56514
  });
56056
56515
  io.stdout.write(rendered.stdout);
56057
56516
  if (rendered.stderr) {
@@ -56121,15 +56580,21 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
56121
56580
  return 0;
56122
56581
  } catch (error51) {
56123
56582
  const envAutoConfirm = process.env[`${cliBrand.envPrefix}_CLI_AUTO_CONFIRM`] === "1";
56124
- if (io.interactive === true || options.autoConfirm === true || envAutoConfirm) {
56125
- const pending = pendingConfirmationFromError2(error51);
56126
- if (pending && pending.commandId === commandId) {
56127
- const reason = io.interactive === true ? "interactive CLI user" : options.autoConfirm === true ? "explicit CLI auto-confirm" : `${cliBrand.envPrefix}_CLI_AUTO_CONFIRM environment auto-confirm`;
56128
- io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the ${reason}.
56129
- `);
56583
+ const pending = pendingConfirmationFromError2(error51);
56584
+ if (pending && pending.commandId === commandId && (io.interactive === true || options.autoConfirm === true || envAutoConfirm)) {
56585
+ renderPendingConfirmationGate3(io, pending);
56586
+ if (io.interactive === true) {
56587
+ const approved = io.confirm !== void 0 && await io.confirm(`Approve ${pending.commandId} (confirmation ${pending.confirmationId})? [y/N]`);
56588
+ if (!approved) {
56589
+ io.stderr.write("Not approved.\n");
56590
+ return 1;
56591
+ }
56130
56592
  try {
56131
- const approved = await client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
56132
- const approvedOutput = asRecord3(approved.output).output ?? approved.output;
56593
+ const approvedResult = await client.execute("confirmation.approve", {
56594
+ confirmation_id: pending.confirmationId,
56595
+ reviewed: true
56596
+ });
56597
+ const approvedOutput = asRecord3(approvedResult.output).output ?? approvedResult.output;
56133
56598
  const rendered = format ? format(approvedOutput) : JSON.stringify(approvedOutput, null, 2);
56134
56599
  io.stdout.write(`${rendered}
56135
56600
  `);
@@ -56138,6 +56603,19 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
56138
56603
  return printCommandError2(io, approvalError);
56139
56604
  }
56140
56605
  }
56606
+ const reason = options.autoConfirm === true ? "explicit CLI auto-confirm" : `${cliBrand.envPrefix}_CLI_AUTO_CONFIRM environment auto-confirm`;
56607
+ io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the ${reason}.
56608
+ `);
56609
+ try {
56610
+ const approvedResult = await client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
56611
+ const approvedOutput = asRecord3(approvedResult.output).output ?? approvedResult.output;
56612
+ const rendered = format ? format(approvedOutput) : JSON.stringify(approvedOutput, null, 2);
56613
+ io.stdout.write(`${rendered}
56614
+ `);
56615
+ return 0;
56616
+ } catch (approvalError) {
56617
+ return printCommandError2(io, approvalError);
56618
+ }
56141
56619
  }
56142
56620
  return printCommandError2(io, error51);
56143
56621
  }
@@ -56160,7 +56638,32 @@ function pendingConfirmationFromError2(error51) {
56160
56638
  if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
56161
56639
  return null;
56162
56640
  }
56163
- return { confirmationId, commandId };
56641
+ const summary = typeof details.summary === "string" ? details.summary : void 0;
56642
+ const riskLevel = typeof details.riskLevel === "string" ? details.riskLevel : void 0;
56643
+ return {
56644
+ confirmationId,
56645
+ commandId,
56646
+ ...summary === void 0 ? {} : { summary },
56647
+ ...riskLevel === void 0 ? {} : { riskLevel },
56648
+ ...details.diff === void 0 ? {} : { diff: details.diff }
56649
+ };
56650
+ }
56651
+ function renderPendingConfirmationGate3(io, pending) {
56652
+ io.stderr.write(`Confirmation required: ${pending.commandId} (${pending.confirmationId})
56653
+ `);
56654
+ if (pending.summary !== void 0) {
56655
+ io.stderr.write(`${redactForLog(pending.summary)}
56656
+ `);
56657
+ }
56658
+ if (pending.riskLevel !== void 0) {
56659
+ io.stderr.write(`Risk level: ${pending.riskLevel}
56660
+ `);
56661
+ }
56662
+ if (pending.diff !== void 0) {
56663
+ io.stderr.write(`Diff:
56664
+ ${redactForLog(pending.diff)}
56665
+ `);
56666
+ }
56164
56667
  }
56165
56668
  function printCommandError2(io, error51) {
56166
56669
  if (error51 instanceof CommandClientError && error51.response && !error51.response.ok) {