stagent 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -0,0 +1,101 @@
1
+ import {
2
+ getRuntimeCatalogEntry,
3
+ type AgentRuntimeId,
4
+ } from "@/lib/agents/runtime/catalog";
5
+
6
+ export interface RuntimeLaunchProgress {
7
+ hasTurnStarted?: boolean;
8
+ hasToolUse?: boolean;
9
+ hasResult?: boolean;
10
+ }
11
+
12
+ export class RetryableRuntimeLaunchError extends Error {
13
+ runtimeId: AgentRuntimeId;
14
+ cause: unknown;
15
+
16
+ constructor(input: {
17
+ runtimeId: AgentRuntimeId;
18
+ message: string;
19
+ cause: unknown;
20
+ }) {
21
+ super(input.message);
22
+ this.name = "RetryableRuntimeLaunchError";
23
+ this.runtimeId = input.runtimeId;
24
+ this.cause = input.cause;
25
+ }
26
+ }
27
+
28
+ function isLikelyRuntimeUnavailableMessage(message: string): boolean {
29
+ const lower = message.toLowerCase();
30
+
31
+ return (
32
+ lower.includes("process exited with code") ||
33
+ lower.includes("command not found") ||
34
+ lower.includes("enoent") ||
35
+ lower.includes("not logged in") ||
36
+ lower.includes("authentication") ||
37
+ lower.includes("oauth") ||
38
+ lower.includes("token expired") ||
39
+ lower.includes("api key") ||
40
+ lower.includes("chatgpt sign-in is not configured") ||
41
+ lower.includes("failed to start") ||
42
+ lower.includes("runtime unavailable")
43
+ );
44
+ }
45
+
46
+ export function classifyTaskFailureReason(error: unknown): string {
47
+ if (!(error instanceof Error)) return "sdk_error";
48
+ if (error.name === "AbortError" || error.message.includes("aborted")) {
49
+ return "aborted";
50
+ }
51
+ const lower = error.message.toLowerCase();
52
+ if (
53
+ lower.includes("turn") &&
54
+ (lower.includes("limit") || lower.includes("exhausted") || lower.includes("max"))
55
+ ) {
56
+ return "turn_limit_exceeded";
57
+ }
58
+ if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
59
+ if (lower.includes("budget")) return "budget_exceeded";
60
+ if (
61
+ lower.includes("authentication") ||
62
+ lower.includes("oauth") ||
63
+ lower.includes("not logged in") ||
64
+ lower.includes("token expired")
65
+ ) {
66
+ return "auth_failed";
67
+ }
68
+ if (lower.includes("rate limit") || lower.includes("429")) {
69
+ return "rate_limited";
70
+ }
71
+ return "sdk_error";
72
+ }
73
+
74
+ export function toRetryableRuntimeLaunchError(input: {
75
+ runtimeId: AgentRuntimeId;
76
+ error: unknown;
77
+ progress: RuntimeLaunchProgress;
78
+ }): RetryableRuntimeLaunchError | null {
79
+ if (!(input.error instanceof Error)) {
80
+ return null;
81
+ }
82
+
83
+ if (
84
+ input.progress.hasTurnStarted ||
85
+ input.progress.hasToolUse ||
86
+ input.progress.hasResult
87
+ ) {
88
+ return null;
89
+ }
90
+
91
+ if (!isLikelyRuntimeUnavailableMessage(input.error.message)) {
92
+ return null;
93
+ }
94
+
95
+ const label = getRuntimeCatalogEntry(input.runtimeId).label;
96
+ return new RetryableRuntimeLaunchError({
97
+ runtimeId: input.runtimeId,
98
+ message: `${label} failed to launch before task execution started: ${input.error.message}`,
99
+ cause: input.error,
100
+ });
101
+ }
@@ -23,6 +23,12 @@ import {
23
23
  readCodexAuthStateFromClient,
24
24
  resolveOpenAICodexAuthContext,
25
25
  } from "./openai-codex-auth";
26
+ import {
27
+ classifyTaskFailureReason,
28
+ RetryableRuntimeLaunchError,
29
+ toRetryableRuntimeLaunchError,
30
+ type RuntimeLaunchProgress,
31
+ } from "./launch-failure";
26
32
  import type {
27
33
  AgentRuntimeAdapter,
28
34
  RuntimeConnectionResult,
@@ -228,6 +234,14 @@ async function finalizeTaskUsage(
228
234
  startedAt: state.startedAt,
229
235
  finishedAt: new Date(),
230
236
  });
237
+
238
+ await db
239
+ .update(tasks)
240
+ .set({
241
+ effectiveModelId: state.modelId ?? null,
242
+ updatedAt: new Date(),
243
+ })
244
+ .where(eq(tasks.id, state.taskId));
231
245
  }
232
246
 
233
247
  function buildTurnInput(prompt: string) {
@@ -373,6 +387,7 @@ async function markTaskFailed(taskId: string, title: string, message: string) {
373
387
  .set({
374
388
  status: "failed",
375
389
  result: message,
390
+ failureReason: classifyTaskFailureReason(new Error(message)),
376
391
  updatedAt: new Date(),
377
392
  })
378
393
  .where(eq(tasks.id, taskId));
@@ -690,6 +705,7 @@ async function executeOpenAICodexTask(
690
705
  let turnId: string | null = null;
691
706
  let agentOutput = "";
692
707
  let settled = false;
708
+ const launchProgress: RuntimeLaunchProgress = {};
693
709
  let resolveCompletion: (() => void) | null = null;
694
710
  let rejectCompletion: ((error: Error) => void) | null = null;
695
711
  const usageState = createTaskUsageState(task, Boolean(task.sessionId));
@@ -704,6 +720,18 @@ async function executeOpenAICodexTask(
704
720
  client = await auth.connect(cwd);
705
721
 
706
722
  client.onProcessError = (error) => {
723
+ const retryableLaunchError =
724
+ !options.resume
725
+ ? toRetryableRuntimeLaunchError({
726
+ runtimeId: "openai-codex-app-server",
727
+ error,
728
+ progress: launchProgress,
729
+ })
730
+ : null;
731
+ if (retryableLaunchError) {
732
+ rejectCompletion?.(retryableLaunchError);
733
+ return;
734
+ }
707
735
  if (settled) return;
708
736
  void settle(async () => {
709
737
  await markTaskFailed(taskId, task.title, error.message);
@@ -744,6 +772,7 @@ async function executeOpenAICodexTask(
744
772
 
745
773
  case "turn/started": {
746
774
  turnId = extractTurnId(params);
775
+ launchProgress.hasTurnStarted = true;
747
776
  setExecution(taskId, {
748
777
  abortController,
749
778
  sessionId: threadId,
@@ -782,6 +811,7 @@ async function executeOpenAICodexTask(
782
811
  }
783
812
 
784
813
  case "item/commandExecution/outputDelta": {
814
+ launchProgress.hasToolUse = true;
785
815
  await insertLog(taskId, "command_output_delta", {
786
816
  threadId,
787
817
  turnId,
@@ -803,6 +833,7 @@ async function executeOpenAICodexTask(
803
833
 
804
834
  case "turn/completed": {
805
835
  const { status, errorMessage } = extractTurnStatus(params);
836
+ launchProgress.hasResult = true;
806
837
 
807
838
  if (status === "completed") {
808
839
  const finalResult =
@@ -917,6 +948,10 @@ async function executeOpenAICodexTask(
917
948
  return;
918
949
  }
919
950
 
951
+ if (error instanceof RetryableRuntimeLaunchError) {
952
+ throw error;
953
+ }
954
+
920
955
  const message = error instanceof Error ? error.message : String(error);
921
956
  await settle(async () => {
922
957
  await markTaskFailed(taskId, task.title, message);
@@ -396,6 +396,14 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
396
396
  startedAt: usageState.startedAt,
397
397
  finishedAt: new Date(),
398
398
  });
399
+
400
+ await db
401
+ .update(tasks)
402
+ .set({
403
+ effectiveModelId: result.totalUsage.modelId ?? modelId,
404
+ updatedAt: new Date(),
405
+ })
406
+ .where(eq(tasks.id, taskId));
399
407
  } catch (err) {
400
408
  if (!abortController.signal.aborted) {
401
409
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,220 @@
1
+ import { db } from "@/lib/db";
2
+ import { agentLogs, notifications, tasks } from "@/lib/db/schema";
3
+ import { eq } from "drizzle-orm";
4
+ import { executeTaskWithRuntime, resumeTaskWithRuntime } from "@/lib/agents/runtime";
5
+ import {
6
+ resolveResumeExecutionTarget,
7
+ resolveTaskExecutionTarget,
8
+ type ResolvedExecutionTarget,
9
+ } from "@/lib/agents/runtime/execution-target";
10
+ import {
11
+ classifyTaskFailureReason,
12
+ RetryableRuntimeLaunchError,
13
+ } from "@/lib/agents/runtime/launch-failure";
14
+ import { getRuntimeCatalogEntry } from "@/lib/agents/runtime/catalog";
15
+
16
+ async function persistExecutionTarget(
17
+ taskId: string,
18
+ target: ResolvedExecutionTarget
19
+ ) {
20
+ await db
21
+ .update(tasks)
22
+ .set({
23
+ effectiveRuntimeId: target.effectiveRuntimeId,
24
+ effectiveModelId: target.effectiveModelId,
25
+ runtimeFallbackReason: target.fallbackReason,
26
+ updatedAt: new Date(),
27
+ })
28
+ .where(eq(tasks.id, taskId));
29
+
30
+ if (target.fallbackApplied && target.fallbackReason) {
31
+ await db.insert(agentLogs).values({
32
+ id: crypto.randomUUID(),
33
+ taskId,
34
+ agentType: "runtime-router",
35
+ event: "runtime_fallback",
36
+ payload: JSON.stringify({
37
+ requestedRuntimeId: target.requestedRuntimeId,
38
+ effectiveRuntimeId: target.effectiveRuntimeId,
39
+ reason: target.fallbackReason,
40
+ }),
41
+ timestamp: new Date(),
42
+ });
43
+ }
44
+ }
45
+
46
+ async function logRuntimeLaunchFailure(
47
+ taskId: string,
48
+ error: RetryableRuntimeLaunchError
49
+ ) {
50
+ await db.insert(agentLogs).values({
51
+ id: crypto.randomUUID(),
52
+ taskId,
53
+ agentType: "runtime-router",
54
+ event: "runtime_launch_failed",
55
+ payload: JSON.stringify({
56
+ runtimeId: error.runtimeId,
57
+ error: error.message,
58
+ }),
59
+ timestamp: new Date(),
60
+ });
61
+ }
62
+
63
+ async function markTaskLaunchFailed(
64
+ taskId: string,
65
+ taskTitle: string,
66
+ error: unknown
67
+ ) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ await db
70
+ .update(tasks)
71
+ .set({
72
+ status: "failed",
73
+ result: message,
74
+ failureReason: classifyTaskFailureReason(
75
+ error instanceof Error ? error : new Error(message)
76
+ ),
77
+ sessionId: null,
78
+ updatedAt: new Date(),
79
+ })
80
+ .where(eq(tasks.id, taskId));
81
+
82
+ await db.insert(notifications).values({
83
+ id: crypto.randomUUID(),
84
+ taskId,
85
+ type: "task_failed",
86
+ title: `Task failed: ${taskTitle}`,
87
+ body: message.slice(0, 500),
88
+ createdAt: new Date(),
89
+ });
90
+ }
91
+
92
+ function buildLaunchFallbackTarget(input: {
93
+ originalTarget: ResolvedExecutionTarget;
94
+ retryTarget: ResolvedExecutionTarget;
95
+ launchError: RetryableRuntimeLaunchError;
96
+ }): ResolvedExecutionTarget {
97
+ const effectiveLabel = getRuntimeCatalogEntry(
98
+ input.retryTarget.effectiveRuntimeId
99
+ ).label;
100
+
101
+ return {
102
+ ...input.retryTarget,
103
+ fallbackApplied: true,
104
+ fallbackReason: `${input.launchError.message}. Fell back to ${effectiveLabel}.`,
105
+ requestedRuntimeId:
106
+ input.retryTarget.requestedRuntimeId ?? input.originalTarget.requestedRuntimeId,
107
+ requestedModelId:
108
+ input.retryTarget.requestedModelId ?? input.originalTarget.requestedModelId,
109
+ effectiveModelId: input.retryTarget.effectiveModelId,
110
+ effectiveRuntimeId: input.retryTarget.effectiveRuntimeId,
111
+ };
112
+ }
113
+
114
+ async function retryTaskWithFallback(
115
+ task: typeof tasks.$inferSelect,
116
+ originalTarget: ResolvedExecutionTarget,
117
+ launchError: RetryableRuntimeLaunchError
118
+ ) {
119
+ await logRuntimeLaunchFailure(task.id, launchError);
120
+
121
+ let retryTarget: ResolvedExecutionTarget;
122
+ try {
123
+ retryTarget = await resolveTaskExecutionTarget({
124
+ title: task.title,
125
+ description: task.description,
126
+ requestedRuntimeId: originalTarget.requestedRuntimeId ?? task.assignedAgent,
127
+ profileId: task.agentProfile,
128
+ unavailableRuntimeIds: [launchError.runtimeId],
129
+ unavailableReasons: {
130
+ [launchError.runtimeId]: launchError.message,
131
+ },
132
+ });
133
+ } catch (error) {
134
+ await markTaskLaunchFailed(task.id, task.title, error);
135
+ throw error;
136
+ }
137
+
138
+ const fallbackTarget = buildLaunchFallbackTarget({
139
+ originalTarget,
140
+ retryTarget,
141
+ launchError,
142
+ });
143
+
144
+ await db
145
+ .update(tasks)
146
+ .set({
147
+ status: "running",
148
+ result: null,
149
+ failureReason: null,
150
+ sessionId: null,
151
+ updatedAt: new Date(),
152
+ })
153
+ .where(eq(tasks.id, task.id));
154
+
155
+ await persistExecutionTarget(task.id, fallbackTarget);
156
+ try {
157
+ return await executeTaskWithRuntime(task.id, fallbackTarget.effectiveRuntimeId);
158
+ } catch (error) {
159
+ if (error instanceof RetryableRuntimeLaunchError) {
160
+ await markTaskLaunchFailed(task.id, task.title, error);
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ export async function startTaskExecution(
167
+ taskId: string,
168
+ options?: { requestedRuntimeId?: string | null }
169
+ ) {
170
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
171
+ if (!task) {
172
+ throw new Error(`Task ${taskId} not found`);
173
+ }
174
+
175
+ const target = await resolveTaskExecutionTarget({
176
+ title: task.title,
177
+ description: task.description,
178
+ requestedRuntimeId: options?.requestedRuntimeId ?? task.assignedAgent,
179
+ profileId: task.agentProfile,
180
+ });
181
+
182
+ await db
183
+ .update(tasks)
184
+ .set({ status: "running", updatedAt: new Date() })
185
+ .where(eq(tasks.id, taskId));
186
+ await persistExecutionTarget(taskId, target);
187
+ try {
188
+ return await executeTaskWithRuntime(taskId, target.effectiveRuntimeId);
189
+ } catch (error) {
190
+ if (error instanceof RetryableRuntimeLaunchError) {
191
+ return retryTaskWithFallback(task, target, error);
192
+ }
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ export async function resumeTaskExecution(
198
+ taskId: string,
199
+ options?: {
200
+ requestedRuntimeId?: string | null;
201
+ effectiveRuntimeId?: string | null;
202
+ }
203
+ ) {
204
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
205
+ if (!task) {
206
+ throw new Error(`Task ${taskId} not found`);
207
+ }
208
+
209
+ const target = await resolveResumeExecutionTarget({
210
+ requestedRuntimeId: options?.requestedRuntimeId ?? task.assignedAgent,
211
+ effectiveRuntimeId: options?.effectiveRuntimeId ?? task.effectiveRuntimeId,
212
+ });
213
+
214
+ await db
215
+ .update(tasks)
216
+ .set({ status: "running", updatedAt: new Date() })
217
+ .where(eq(tasks.id, taskId));
218
+ await persistExecutionTarget(taskId, target);
219
+ return resumeTaskWithRuntime(taskId, target.effectiveRuntimeId);
220
+ }
@@ -12,6 +12,7 @@ import { notifications } from "@/lib/db/schema";
12
12
  import { eq } from "drizzle-orm";
13
13
  import type { CanUseToolPolicy } from "./profiles/types";
14
14
  import { isExaTool, isExaReadOnly } from "./browser-mcp";
15
+ import { CLAUDE_SDK_READ_ONLY_FS_TOOLS } from "./runtime/claude-sdk";
15
16
 
16
17
  // ── Types ────────────────────────────────────────────────────────────
17
18
 
@@ -120,7 +121,10 @@ export async function handleToolPermission(
120
121
  ): Promise<ToolPermissionResponse> {
121
122
  const isQuestion = toolName === "AskUserQuestion";
122
123
 
123
- // Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O
124
+ // Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O.
125
+ // Runs BEFORE Layer 1.75's SDK filesystem auto-allow so `autoDeny: ["Read"]`
126
+ // still denies; `autoApprove` for Read/Grep/Glob is redundant (Layer 1.75
127
+ // would also allow) but harmless.
124
128
  if (!isQuestion && canUseToolPolicy) {
125
129
  if (canUseToolPolicy.autoApprove?.includes(toolName)) {
126
130
  return buildAllowedToolPermissionResponse(input);
@@ -135,6 +139,17 @@ export async function handleToolPermission(
135
139
  return buildAllowedToolPermissionResponse(input);
136
140
  }
137
141
 
142
+ // Layer 1.75: SDK filesystem read-only tools and Skill invocations —
143
+ // auto-approve without I/O. Mirrors the chat-side Phase 1a policy
144
+ // (src/lib/chat/engine.ts canUseTool). Read/Grep/Glob are non-destructive;
145
+ // Skill load is equivalent to using `claude` CLI directly — any tool the
146
+ // loaded skill subsequently invokes (Bash, Edit, etc.) goes through this
147
+ // same canUseTool check. See features/chat-claude-sdk-skills.md Error
148
+ // & Rescue Registry row "settingSources loads hostile skill."
149
+ if (!isQuestion && (CLAUDE_SDK_READ_ONLY_FS_TOOLS.has(toolName) || toolName === "Skill")) {
150
+ return buildAllowedToolPermissionResponse(input);
151
+ }
152
+
138
153
  // Layer 2: Saved user permissions — skip notification for pre-approved tools
139
154
  if (!isQuestion) {
140
155
  const { isToolAllowed } = await import("@/lib/settings/permissions");