stagent 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +70 -23
  2. package/dist/cli.js +44 -10
  3. package/docs/.last-generated +1 -1
  4. package/docs/features/chat.md +54 -49
  5. package/docs/features/schedules.md +38 -32
  6. package/docs/features/settings.md +105 -50
  7. package/docs/manifest.json +8 -8
  8. package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
  9. package/drizzle.config.ts +3 -1
  10. package/package.json +5 -1
  11. package/src/app/api/book/bookmarks/route.ts +73 -0
  12. package/src/app/api/book/progress/route.ts +79 -0
  13. package/src/app/api/book/regenerate/route.ts +111 -0
  14. package/src/app/api/book/stage/route.ts +13 -0
  15. package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
  16. package/src/app/api/chat/conversations/[id]/respond/route.ts +19 -20
  17. package/src/app/api/chat/conversations/[id]/route.ts +2 -1
  18. package/src/app/api/chat/entities/search/route.ts +97 -0
  19. package/src/app/api/documents/[id]/file/route.ts +4 -1
  20. package/src/app/api/documents/[id]/route.ts +34 -2
  21. package/src/app/api/documents/route.ts +91 -0
  22. package/src/app/api/projects/[id]/route.ts +119 -9
  23. package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
  24. package/src/app/api/settings/browser-tools/route.ts +68 -0
  25. package/src/app/api/settings/runtime/route.ts +29 -8
  26. package/src/app/book/page.tsx +14 -0
  27. package/src/app/chat/page.tsx +7 -1
  28. package/src/app/globals.css +375 -0
  29. package/src/app/projects/[id]/page.tsx +31 -6
  30. package/src/app/settings/page.tsx +2 -0
  31. package/src/app/{playbook → user-guide}/[slug]/page.tsx +12 -2
  32. package/src/app/{playbook → user-guide}/page.tsx +2 -2
  33. package/src/app/workflows/[id]/page.tsx +28 -2
  34. package/src/components/book/book-reader.tsx +801 -0
  35. package/src/components/book/chapter-generation-bar.tsx +109 -0
  36. package/src/components/book/content-blocks.tsx +432 -0
  37. package/src/components/book/path-progress.tsx +33 -0
  38. package/src/components/book/path-selector.tsx +42 -0
  39. package/src/components/book/try-it-now.tsx +164 -0
  40. package/src/components/chat/chat-activity-indicator.tsx +92 -0
  41. package/src/components/chat/chat-command-popover.tsx +277 -0
  42. package/src/components/chat/chat-input.tsx +85 -10
  43. package/src/components/chat/chat-message-list.tsx +3 -0
  44. package/src/components/chat/chat-message.tsx +29 -7
  45. package/src/components/chat/chat-permission-request.tsx +5 -1
  46. package/src/components/chat/chat-question.tsx +3 -0
  47. package/src/components/chat/chat-shell.tsx +159 -24
  48. package/src/components/chat/conversation-list.tsx +8 -2
  49. package/src/components/chat/screenshot-gallery.tsx +96 -0
  50. package/src/components/monitoring/log-entry.tsx +61 -27
  51. package/src/components/playbook/adoption-heatmap.tsx +1 -1
  52. package/src/components/playbook/journey-card.tsx +1 -1
  53. package/src/components/playbook/playbook-card.tsx +1 -1
  54. package/src/components/playbook/playbook-detail-view.tsx +15 -5
  55. package/src/components/playbook/playbook-homepage.tsx +1 -1
  56. package/src/components/playbook/playbook-updated-badge.tsx +1 -1
  57. package/src/components/projects/project-detail.tsx +160 -27
  58. package/src/components/projects/project-form-sheet.tsx +6 -2
  59. package/src/components/projects/project-list.tsx +1 -1
  60. package/src/components/schedules/schedule-create-sheet.tsx +24 -330
  61. package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
  62. package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
  63. package/src/components/schedules/schedule-form.tsx +410 -0
  64. package/src/components/schedules/schedule-list.tsx +16 -0
  65. package/src/components/settings/browser-tools-section.tsx +247 -0
  66. package/src/components/settings/runtime-timeout-section.tsx +117 -37
  67. package/src/components/shared/app-sidebar.tsx +7 -1
  68. package/src/components/shared/command-palette.tsx +4 -33
  69. package/src/components/shared/screenshot-lightbox.tsx +151 -0
  70. package/src/hooks/use-caret-position.ts +104 -0
  71. package/src/hooks/use-chapter-generation.ts +255 -0
  72. package/src/hooks/use-chat-autocomplete.ts +290 -0
  73. package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
  74. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  75. package/src/lib/agents/browser-mcp.ts +119 -0
  76. package/src/lib/agents/claude-agent.ts +78 -14
  77. package/src/lib/book/chapter-generator.ts +193 -0
  78. package/src/lib/book/chapter-mapping.ts +91 -0
  79. package/src/lib/book/content.ts +251 -0
  80. package/src/lib/book/markdown-parser.ts +317 -0
  81. package/src/lib/book/reading-paths.ts +82 -0
  82. package/src/lib/book/types.ts +152 -0
  83. package/src/lib/book/update-detector.ts +157 -0
  84. package/src/lib/chat/codex-engine.ts +537 -0
  85. package/src/lib/chat/command-data.ts +50 -0
  86. package/src/lib/chat/context-builder.ts +145 -7
  87. package/src/lib/chat/engine.ts +207 -49
  88. package/src/lib/chat/model-discovery.ts +13 -5
  89. package/src/lib/chat/permission-bridge.ts +14 -2
  90. package/src/lib/chat/slash-commands.ts +191 -0
  91. package/src/lib/chat/stagent-tools.ts +2 -0
  92. package/src/lib/chat/system-prompt.ts +16 -1
  93. package/src/lib/chat/tool-catalog.ts +185 -0
  94. package/src/lib/chat/tools/chat-history-tools.ts +177 -0
  95. package/src/lib/chat/tools/document-tools.ts +241 -0
  96. package/src/lib/chat/tools/settings-tools.ts +29 -3
  97. package/src/lib/chat/types.ts +19 -2
  98. package/src/lib/constants/settings.ts +5 -0
  99. package/src/lib/data/chat.ts +83 -2
  100. package/src/lib/data/clear.ts +24 -4
  101. package/src/lib/db/bootstrap.ts +29 -0
  102. package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
  103. package/src/lib/db/schema.ts +37 -0
  104. package/src/lib/docs/types.ts +9 -0
  105. package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
  106. package/src/lib/screenshots/persist.ts +114 -0
  107. package/src/lib/utils/stagent-paths.ts +4 -0
  108. /package/src/app/api/{playbook → user-guide}/status/route.ts +0 -0
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Mock the settings helpers before importing the module under test
4
+ vi.mock("@/lib/settings/helpers", () => ({
5
+ getSetting: vi.fn(),
6
+ }));
7
+
8
+ import { getSetting } from "@/lib/settings/helpers";
9
+ import {
10
+ getBrowserMcpServers,
11
+ getBrowserAllowedToolPatterns,
12
+ isBrowserTool,
13
+ isBrowserReadOnly,
14
+ } from "@/lib/agents/browser-mcp";
15
+
16
+ const mockGetSetting = vi.mocked(getSetting);
17
+
18
+ describe("browser-mcp", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ describe("getBrowserMcpServers", () => {
24
+ it("returns empty object when neither server is enabled", async () => {
25
+ mockGetSetting.mockResolvedValue(null);
26
+ const servers = await getBrowserMcpServers();
27
+ expect(servers).toEqual({});
28
+ });
29
+
30
+ it("returns chrome-devtools config when enabled", async () => {
31
+ mockGetSetting.mockImplementation(async (key: string) => {
32
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
33
+ return null;
34
+ });
35
+
36
+ const servers = await getBrowserMcpServers();
37
+ expect(servers["chrome-devtools"]).toBeDefined();
38
+ expect(servers["chrome-devtools"].command).toBe("npx");
39
+ expect(servers["chrome-devtools"].args).toContain("chrome-devtools-mcp@latest");
40
+ expect(servers.playwright).toBeUndefined();
41
+ });
42
+
43
+ it("returns playwright config when enabled", async () => {
44
+ mockGetSetting.mockImplementation(async (key: string) => {
45
+ if (key === "browser.playwrightEnabled") return "true";
46
+ return null;
47
+ });
48
+
49
+ const servers = await getBrowserMcpServers();
50
+ expect(servers.playwright).toBeDefined();
51
+ expect(servers.playwright.command).toBe("npx");
52
+ expect(servers.playwright.args).toContain("@playwright/mcp@latest");
53
+ expect(servers["chrome-devtools"]).toBeUndefined();
54
+ });
55
+
56
+ it("returns both when both enabled", async () => {
57
+ mockGetSetting.mockImplementation(async (key: string) => {
58
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
59
+ if (key === "browser.playwrightEnabled") return "true";
60
+ return null;
61
+ });
62
+
63
+ const servers = await getBrowserMcpServers();
64
+ expect(Object.keys(servers)).toHaveLength(2);
65
+ expect(servers["chrome-devtools"]).toBeDefined();
66
+ expect(servers.playwright).toBeDefined();
67
+ });
68
+
69
+ it("appends extra args from config", async () => {
70
+ mockGetSetting.mockImplementation(async (key: string) => {
71
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
72
+ if (key === "browser.chromeDevtoolsConfig") return "--headless --browser-url http://localhost:9222";
73
+ return null;
74
+ });
75
+
76
+ const servers = await getBrowserMcpServers();
77
+ expect(servers["chrome-devtools"].args).toContain("--headless");
78
+ expect(servers["chrome-devtools"].args).toContain("--browser-url");
79
+ expect(servers["chrome-devtools"].args).toContain("http://localhost:9222");
80
+ });
81
+
82
+ it("handles JSON array config format", async () => {
83
+ mockGetSetting.mockImplementation(async (key: string) => {
84
+ if (key === "browser.playwrightEnabled") return "true";
85
+ if (key === "browser.playwrightConfig") return '["--browser", "firefox"]';
86
+ return null;
87
+ });
88
+
89
+ const servers = await getBrowserMcpServers();
90
+ expect(servers.playwright.args).toContain("--browser");
91
+ expect(servers.playwright.args).toContain("firefox");
92
+ });
93
+
94
+ it("handles invalid JSON config as space-separated args", async () => {
95
+ mockGetSetting.mockImplementation(async (key: string) => {
96
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
97
+ if (key === "browser.chromeDevtoolsConfig") return "[invalid json";
98
+ return null;
99
+ });
100
+
101
+ const servers = await getBrowserMcpServers();
102
+ // Invalid JSON starting with [ falls back to space-split
103
+ expect(servers["chrome-devtools"].args).toEqual(["-y", "chrome-devtools-mcp@latest", "[invalid", "json"]);
104
+ });
105
+ });
106
+
107
+ describe("getBrowserAllowedToolPatterns", () => {
108
+ it("returns empty when nothing enabled", async () => {
109
+ mockGetSetting.mockResolvedValue(null);
110
+ const patterns = await getBrowserAllowedToolPatterns();
111
+ expect(patterns).toEqual([]);
112
+ });
113
+
114
+ it("returns chrome pattern when chrome enabled", async () => {
115
+ mockGetSetting.mockImplementation(async (key: string) => {
116
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
117
+ return null;
118
+ });
119
+
120
+ const patterns = await getBrowserAllowedToolPatterns();
121
+ expect(patterns).toContain("mcp__chrome-devtools__*");
122
+ expect(patterns).not.toContain("mcp__playwright__*");
123
+ });
124
+
125
+ it("returns both patterns when both enabled", async () => {
126
+ mockGetSetting.mockImplementation(async (key: string) => {
127
+ if (key === "browser.chromeDevtoolsEnabled") return "true";
128
+ if (key === "browser.playwrightEnabled") return "true";
129
+ return null;
130
+ });
131
+
132
+ const patterns = await getBrowserAllowedToolPatterns();
133
+ expect(patterns).toContain("mcp__chrome-devtools__*");
134
+ expect(patterns).toContain("mcp__playwright__*");
135
+ });
136
+ });
137
+
138
+ describe("isBrowserTool", () => {
139
+ it("identifies chrome devtools tools", () => {
140
+ expect(isBrowserTool("mcp__chrome-devtools__click")).toBe(true);
141
+ expect(isBrowserTool("mcp__chrome-devtools__take_screenshot")).toBe(true);
142
+ });
143
+
144
+ it("identifies playwright tools", () => {
145
+ expect(isBrowserTool("mcp__playwright__browser_navigate")).toBe(true);
146
+ expect(isBrowserTool("mcp__playwright__browser_snapshot")).toBe(true);
147
+ });
148
+
149
+ it("rejects non-browser tools", () => {
150
+ expect(isBrowserTool("mcp__stagent__list_tasks")).toBe(false);
151
+ expect(isBrowserTool("Read")).toBe(false);
152
+ expect(isBrowserTool("Bash")).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe("isBrowserReadOnly", () => {
157
+ it("identifies read-only chrome devtools tools", () => {
158
+ expect(isBrowserReadOnly("mcp__chrome-devtools__take_screenshot")).toBe(true);
159
+ expect(isBrowserReadOnly("mcp__chrome-devtools__list_pages")).toBe(true);
160
+ expect(isBrowserReadOnly("mcp__chrome-devtools__lighthouse_audit")).toBe(true);
161
+ });
162
+
163
+ it("identifies read-only playwright tools", () => {
164
+ expect(isBrowserReadOnly("mcp__playwright__browser_snapshot")).toBe(true);
165
+ expect(isBrowserReadOnly("mcp__playwright__browser_tabs")).toBe(true);
166
+ });
167
+
168
+ it("rejects mutation browser tools", () => {
169
+ expect(isBrowserReadOnly("mcp__chrome-devtools__click")).toBe(false);
170
+ expect(isBrowserReadOnly("mcp__chrome-devtools__fill")).toBe(false);
171
+ expect(isBrowserReadOnly("mcp__playwright__browser_navigate")).toBe(false);
172
+ expect(isBrowserReadOnly("mcp__playwright__browser_click")).toBe(false);
173
+ });
174
+ });
175
+ });
@@ -132,6 +132,9 @@ vi.mock("@/lib/agents/pattern-extractor", () => ({
132
132
  vi.mock("@/lib/agents/sweep", () => ({
133
133
  processSweepResult: mockProcessSweepResult,
134
134
  }));
135
+ vi.mock("@/lib/agents/browser-mcp", () => ({
136
+ getBrowserMcpServers: vi.fn().mockResolvedValue({}),
137
+ }));
135
138
 
136
139
  // Static imports (works because vi.mock is hoisted)
137
140
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -0,0 +1,119 @@
1
+ import { getSetting } from "@/lib/settings/helpers";
2
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
3
+
4
+ // ── MCP server config type (matches Claude Agent SDK shape) ──────────
5
+
6
+ interface McpServerConfig {
7
+ command: string;
8
+ args: string[];
9
+ }
10
+
11
+ // ── Read-only browser tools — auto-approved in chat & task permission callbacks
12
+
13
+ export const BROWSER_READ_ONLY_TOOLS = new Set([
14
+ // Chrome DevTools MCP — read-only
15
+ "mcp__chrome-devtools__take_screenshot",
16
+ "mcp__chrome-devtools__take_snapshot",
17
+ "mcp__chrome-devtools__take_memory_snapshot",
18
+ "mcp__chrome-devtools__list_pages",
19
+ "mcp__chrome-devtools__list_console_messages",
20
+ "mcp__chrome-devtools__list_network_requests",
21
+ "mcp__chrome-devtools__get_console_message",
22
+ "mcp__chrome-devtools__get_network_request",
23
+ "mcp__chrome-devtools__lighthouse_audit",
24
+ "mcp__chrome-devtools__performance_start_trace",
25
+ "mcp__chrome-devtools__performance_stop_trace",
26
+ "mcp__chrome-devtools__performance_analyze_insight",
27
+ // Playwright MCP — read-only
28
+ "mcp__playwright__browser_snapshot",
29
+ "mcp__playwright__browser_console_messages",
30
+ "mcp__playwright__browser_network_requests",
31
+ "mcp__playwright__browser_tabs",
32
+ "mcp__playwright__browser_take_screenshot",
33
+ ]);
34
+
35
+ // ── Helper: check if a tool name belongs to a browser MCP server ─────
36
+
37
+ export function isBrowserTool(toolName: string): boolean {
38
+ return (
39
+ toolName.startsWith("mcp__chrome-devtools__") ||
40
+ toolName.startsWith("mcp__playwright__")
41
+ );
42
+ }
43
+
44
+ export function isBrowserReadOnly(toolName: string): boolean {
45
+ return BROWSER_READ_ONLY_TOOLS.has(toolName);
46
+ }
47
+
48
+ // ── Config builder ───────────────────────────────────────────────────
49
+
50
+ function parseExtraArgs(config: string | null): string[] {
51
+ if (!config) return [];
52
+ const trimmed = config.trim();
53
+ if (!trimmed) return [];
54
+
55
+ // Try JSON array first (e.g. '["--browser", "firefox"]')
56
+ if (trimmed.startsWith("[")) {
57
+ try {
58
+ const parsed = JSON.parse(trimmed);
59
+ if (Array.isArray(parsed)) return parsed.filter((a): a is string => typeof a === "string");
60
+ } catch {
61
+ // Fall through to space-split
62
+ }
63
+ }
64
+
65
+ // Plain string: split on whitespace (e.g. "--headless --browser-url http://localhost:9222")
66
+ return trimmed.split(/\s+/).filter(Boolean);
67
+ }
68
+
69
+ /**
70
+ * Read browser MCP settings from DB and return MCP server configs
71
+ * for any enabled browser servers.
72
+ *
73
+ * Returns `{}` when neither server is enabled — zero overhead.
74
+ */
75
+ export async function getBrowserMcpServers(): Promise<Record<string, McpServerConfig>> {
76
+ const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
77
+ await Promise.all([
78
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
79
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
80
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
81
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
82
+ ]);
83
+
84
+ const servers: Record<string, McpServerConfig> = {};
85
+
86
+ if (chromeEnabled === "true") {
87
+ const extraArgs = parseExtraArgs(chromeConfig);
88
+ servers["chrome-devtools"] = {
89
+ command: "npx",
90
+ args: ["-y", "chrome-devtools-mcp@latest", ...extraArgs],
91
+ };
92
+ }
93
+
94
+ if (playwrightEnabled === "true") {
95
+ const extraArgs = parseExtraArgs(playwrightConfig);
96
+ servers.playwright = {
97
+ command: "npx",
98
+ args: ["-y", "@playwright/mcp@latest", ...extraArgs],
99
+ };
100
+ }
101
+
102
+ return servers;
103
+ }
104
+
105
+ /**
106
+ * Build the allowedTools glob patterns for enabled browser MCP servers.
107
+ * Returns an empty array when no browser servers are enabled.
108
+ */
109
+ export async function getBrowserAllowedToolPatterns(): Promise<string[]> {
110
+ const [chromeEnabled, playwrightEnabled] = await Promise.all([
111
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
112
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
113
+ ]);
114
+
115
+ const patterns: string[] = [];
116
+ if (chromeEnabled === "true") patterns.push("mcp__chrome-devtools__*");
117
+ if (playwrightEnabled === "true") patterns.push("mcp__playwright__*");
118
+ return patterns;
119
+ }
@@ -17,9 +17,11 @@ import { resolveProfileRuntimePayload, type ResolvedProfileRuntimePayload } from
17
17
  import type { CanUseToolPolicy } from "./profiles/types";
18
18
  import { buildClaudeSdkEnv } from "./runtime/claude-sdk";
19
19
  import { getActiveLearnedContext } from "./learned-context";
20
- import { getLaunchCwd } from "@/lib/environment/workspace-context";
20
+ import { getLaunchCwd, getWorkspaceContext } from "@/lib/environment/workspace-context";
21
21
  import { analyzeForLearnedPatterns } from "./pattern-extractor";
22
22
  import { processSweepResult } from "./sweep";
23
+ import { getBrowserMcpServers } from "./browser-mcp";
24
+ import { persistScreenshot, SCREENSHOT_TOOL_NAMES } from "@/lib/screenshots/persist";
23
25
  import {
24
26
  extractUsageSnapshot,
25
27
  mergeUsageSnapshot,
@@ -210,6 +212,9 @@ async function processAgentStream(
210
212
  let receivedResult = false;
211
213
  let turnCount = 0;
212
214
 
215
+ // Screenshot interception state
216
+ const pendingScreenshotTools = new Set<string>();
217
+
213
218
  for await (const raw of response) {
214
219
  const message = raw as AgentStreamMessage;
215
220
  applyUsageSnapshot(usageState, raw);
@@ -266,6 +271,10 @@ async function processAgentStream(
266
271
  turnCount++;
267
272
  for (const block of message.message.content) {
268
273
  if (block.type === "tool_use") {
274
+ // Track screenshot tool_use IDs for result interception
275
+ if (typeof block.name === "string" && SCREENSHOT_TOOL_NAMES.has(block.name) && typeof block.id === "string") {
276
+ pendingScreenshotTools.add(block.id);
277
+ }
269
278
  await db.insert(agentLogs).values({
270
279
  id: crypto.randomUUID(),
271
280
  taskId,
@@ -281,6 +290,47 @@ async function processAgentStream(
281
290
  }
282
291
  }
283
292
 
293
+ // Intercept tool results containing screenshot image data
294
+ if (message.type === "user" && pendingScreenshotTools.size > 0) {
295
+ const userMsg = (raw as Record<string, unknown>).message as Record<string, unknown> | undefined;
296
+ const userContent = userMsg?.content as Array<Record<string, unknown>> | undefined;
297
+ if (userContent) {
298
+ for (const block of userContent) {
299
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string" && pendingScreenshotTools.has(block.tool_use_id)) {
300
+ pendingScreenshotTools.delete(block.tool_use_id);
301
+ const resultContent = block.content as Array<Record<string, unknown>> | undefined;
302
+ if (resultContent) {
303
+ for (const item of resultContent) {
304
+ if (item.type === "image" && typeof item.source === "object" && item.source !== null) {
305
+ const source = item.source as Record<string, unknown>;
306
+ if (source.type === "base64" && typeof source.data === "string") {
307
+ const attachment = await persistScreenshot(source.data, {
308
+ taskId,
309
+ toolName: `screenshot_${block.tool_use_id}`,
310
+ });
311
+ if (attachment) {
312
+ await db.insert(agentLogs).values({
313
+ id: crypto.randomUUID(),
314
+ taskId,
315
+ agentType: agentProfileId,
316
+ event: "screenshot",
317
+ payload: JSON.stringify({
318
+ documentId: attachment.documentId,
319
+ thumbnailUrl: attachment.thumbnailUrl,
320
+ toolName: `screenshot_${block.tool_use_id}`,
321
+ }),
322
+ timestamp: new Date(),
323
+ });
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+
284
334
  // Handle result — skip if task was cancelled mid-stream
285
335
  if (message.type === "result" && "result" in raw) {
286
336
  if (abortController.signal.aborted) {
@@ -415,11 +465,6 @@ async function buildTaskQueryContext(
415
465
  ? `## Learned Context\nPatterns and insights learned from previous tasks:\n\n${learnedCtx}`
416
466
  : "";
417
467
 
418
- // F1: Separate system instructions from user content
419
- const systemInstructions = [profileInstructions, learnedCtxBlock, docContext, outputInstructions]
420
- .filter(Boolean)
421
- .join("\n\n");
422
-
423
468
  // Resolve working directory: project's workingDirectory > launch cwd
424
469
  let cwd = getLaunchCwd();
425
470
  if (task.projectId) {
@@ -432,6 +477,17 @@ async function buildTaskQueryContext(
432
477
  }
433
478
  }
434
479
 
480
+ // Add worktree guidance when running inside a git worktree
481
+ const ws = getWorkspaceContext();
482
+ const worktreeNote = ws.isWorktree
483
+ ? `## Workspace Note\nYou are operating inside a git worktree (branch: ${ws.gitBranch ?? "unknown"}). All file operations MUST use paths relative to the working directory: ${cwd}. Do NOT navigate to or create files in the main repository directory.`
484
+ : "";
485
+
486
+ // F1: Separate system instructions from user content
487
+ const systemInstructions = [worktreeNote, profileInstructions, learnedCtxBlock, docContext, outputInstructions]
488
+ .filter(Boolean)
489
+ .join("\n\n");
490
+
435
491
  // F9: Use profile maxTurns or fall back to default
436
492
  const maxTurns = profile?.maxTurns ?? DEFAULT_MAX_TURNS;
437
493
 
@@ -464,6 +520,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
464
520
  await prepareTaskOutputDirectory(taskId, { clearExisting: true });
465
521
  const ctx = await buildTaskQueryContext(task, agentProfileId);
466
522
 
523
+ // Merge browser MCP servers when enabled globally
524
+ const browserServers = await getBrowserMcpServers();
525
+ const profileMcpServers = ctx.payload?.mcpServers ?? {};
526
+ const mergedMcpServers = { ...profileMcpServers, ...browserServers };
527
+
467
528
  const authEnv = await getAuthEnv();
468
529
  const response = query({
469
530
  prompt: ctx.userPrompt,
@@ -481,10 +542,9 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
481
542
  // F4: Per-execution budget cap
482
543
  maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
483
544
  ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
484
- ...(ctx.payload?.mcpServers &&
485
- Object.keys(ctx.payload.mcpServers).length > 0 && {
486
- mcpServers: ctx.payload.mcpServers,
487
- }),
545
+ ...(Object.keys(mergedMcpServers).length > 0 && {
546
+ mcpServers: mergedMcpServers,
547
+ }),
488
548
  // @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
489
549
  canUseTool: async (
490
550
  toolName: string,
@@ -570,6 +630,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
570
630
  await prepareTaskOutputDirectory(taskId);
571
631
  const ctx = await buildTaskQueryContext(task, profileId);
572
632
 
633
+ // Merge browser MCP servers when enabled globally
634
+ const browserServers = await getBrowserMcpServers();
635
+ const profileMcpServers = ctx.payload?.mcpServers ?? {};
636
+ const mergedMcpServers = { ...profileMcpServers, ...browserServers };
637
+
573
638
  const authEnv = await getAuthEnv();
574
639
  const response = query({
575
640
  prompt: ctx.userPrompt,
@@ -588,10 +653,9 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
588
653
  // F4: Per-execution budget cap
589
654
  maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
590
655
  ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
591
- ...(ctx.payload?.mcpServers &&
592
- Object.keys(ctx.payload.mcpServers).length > 0 && {
593
- mcpServers: ctx.payload.mcpServers,
594
- }),
656
+ ...(Object.keys(mergedMcpServers).length > 0 && {
657
+ mcpServers: mergedMcpServers,
658
+ }),
595
659
  // @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
596
660
  canUseTool: async (
597
661
  toolName: string,
@@ -0,0 +1,193 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { CHAPTER_MAPPING } from "./chapter-mapping";
4
+ import { getChapter } from "./content";
5
+
6
+ /** Shared context gathered from disk for prompt assembly */
7
+ interface ChapterContext {
8
+ chapterId: string;
9
+ chapterNumber: number;
10
+ title: string;
11
+ subtitle: string;
12
+ partNumber: number;
13
+ partTitle: string;
14
+ readingTime: number;
15
+ slug: string;
16
+ relatedDocs: string[];
17
+ relatedJourney: string | undefined;
18
+ currentMarkdown: string | null;
19
+ sourceContents: string[];
20
+ strategy: string | null;
21
+ }
22
+
23
+ /** Gather all context needed for chapter generation from disk */
24
+ export function gatherChapterContext(chapterId: string): ChapterContext {
25
+ const chapter = getChapter(chapterId);
26
+ if (!chapter) {
27
+ throw new Error(`Chapter not found: ${chapterId}`);
28
+ }
29
+
30
+ const mapping = CHAPTER_MAPPING[chapterId];
31
+ const sourceDocSlugs = mapping?.docs ?? [];
32
+ const slug = chapterIdToSlug(chapterId);
33
+
34
+ // Read the current chapter markdown (if it exists)
35
+ const chapterMdPath = join(process.cwd(), "book", "chapters", `${slug}.md`);
36
+ const currentMarkdown = existsSync(chapterMdPath)
37
+ ? readFileSync(chapterMdPath, "utf-8")
38
+ : null;
39
+
40
+ // Read related playbook docs for content
41
+ const sourceContents: string[] = [];
42
+ for (const docSlug of sourceDocSlugs) {
43
+ const docPath = join(process.cwd(), "docs", "features", `${docSlug}.md`);
44
+ if (existsSync(docPath)) {
45
+ const content = readFileSync(docPath, "utf-8");
46
+ sourceContents.push(`### Feature: ${docSlug}\n${content}`);
47
+ }
48
+ }
49
+
50
+ // Read the book strategy document
51
+ const strategyPath = join(process.cwd(), "ai-native-notes", "ai-native-book-strategy.md");
52
+ const strategy = existsSync(strategyPath)
53
+ ? readFileSync(strategyPath, "utf-8")
54
+ : null;
55
+
56
+ return {
57
+ chapterId,
58
+ chapterNumber: chapter.number,
59
+ title: chapter.title,
60
+ subtitle: chapter.subtitle,
61
+ partNumber: chapter.part.number,
62
+ partTitle: chapter.part.title,
63
+ readingTime: chapter.readingTime,
64
+ slug,
65
+ relatedDocs: chapter.relatedDocs ?? [],
66
+ relatedJourney: chapter.relatedJourney,
67
+ currentMarkdown,
68
+ sourceContents,
69
+ strategy,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Build an agent prompt for generating or regenerating a book chapter.
75
+ * Assembles context from the current chapter, source files,
76
+ * and the book strategy document.
77
+ */
78
+ export function buildChapterRegenerationPrompt(chapterId: string): string {
79
+ const ctx = gatherChapterContext(chapterId);
80
+ const isNew = ctx.currentMarkdown === null;
81
+ const verb = isNew ? "generating" : "regenerating";
82
+ const outputPath = `book/chapters/${ctx.slug}.md`;
83
+
84
+ // Assemble the prompt
85
+ const sections: string[] = [
86
+ `# Chapter ${isNew ? "Generation" : "Regeneration"}: ${ctx.title}`,
87
+ "",
88
+ `You are ${verb} Chapter ${ctx.chapterNumber}: "${ctx.title}" — ${ctx.subtitle}`,
89
+ `This chapter belongs to Part ${ctx.partNumber}: ${ctx.partTitle}.`,
90
+ "",
91
+ "## Instructions",
92
+ "",
93
+ `${isNew ? "Write" : "Regenerate"} this book chapter following these rules:`,
94
+ "1. **Narrative voice**: Write in first-person plural ('we') with a technical-but-approachable tone",
95
+ "2. **Structure**: Follow the Problem → Solution → Implementation → Lessons pattern",
96
+ "3. **Code examples**: Include real code snippets from the Stagent codebase with filename comments",
97
+ "4. **Markdown format**: Use the conventions below for content blocks",
98
+ "5. **Reading time**: Target approximately " + ctx.readingTime + " minutes",
99
+ ...(isNew ? [] : ["6. **Preserve Author's Notes**: Keep any existing `> [!authors-note]` blocks unchanged"]),
100
+ "",
101
+ "## Tools Available",
102
+ "",
103
+ "You have access to **Read**, **Write**, and **Edit** tools.",
104
+ "- Use **Read** to examine Stagent source code files for real code examples",
105
+ "- Use **Write** to create the final chapter file",
106
+ "",
107
+ "## Markdown Conventions",
108
+ "",
109
+ "- Sections: `## Section Title`",
110
+ "- Code blocks: ` ```language ` with `<!-- filename: path -->` before the block",
111
+ "- Callouts: `> [!tip]`, `> [!warning]`, `> [!info]`, `> [!lesson]`, `> [!authors-note]`",
112
+ "- Interactive links: `[Try: label](href)`",
113
+ "- Images: `![alt](src \"caption\")`",
114
+ "",
115
+ ];
116
+
117
+ if (ctx.currentMarkdown) {
118
+ sections.push(
119
+ "## Current Chapter Content",
120
+ "",
121
+ "Update this content to reflect any changes in the source material:",
122
+ "",
123
+ "```markdown",
124
+ ctx.currentMarkdown,
125
+ "```",
126
+ ""
127
+ );
128
+ }
129
+
130
+ if (ctx.sourceContents.length > 0) {
131
+ sections.push(
132
+ "## Source Material (Playbook Feature Docs)",
133
+ "",
134
+ "Use these feature docs as the primary source of technical details:",
135
+ "",
136
+ ...ctx.sourceContents,
137
+ ""
138
+ );
139
+ }
140
+
141
+ if (ctx.strategy) {
142
+ sections.push(
143
+ "## Book Strategy Reference",
144
+ "",
145
+ "Follow the themes and narrative arc described here:",
146
+ "",
147
+ ctx.strategy.slice(0, 4000), // Truncate to avoid excessive context
148
+ ""
149
+ );
150
+ }
151
+
152
+ const now = new Date().toISOString();
153
+ sections.push(
154
+ "## Output",
155
+ "",
156
+ `Write the chapter to the file: \`${outputPath}\``,
157
+ "",
158
+ "The file must include this frontmatter at the top:",
159
+ "",
160
+ "```yaml",
161
+ "---",
162
+ `title: "${ctx.title}"`,
163
+ `subtitle: "${ctx.subtitle}"`,
164
+ `chapter: ${ctx.chapterNumber}`,
165
+ `part: ${ctx.partNumber}`,
166
+ `readingTime: ${ctx.readingTime}`,
167
+ `lastGeneratedBy: "${now}"`,
168
+ ...(ctx.relatedDocs.length > 0 ? [`relatedDocs: ${JSON.stringify(ctx.relatedDocs)}`] : []),
169
+ ...(ctx.relatedJourney ? [`relatedJourney: "${ctx.relatedJourney}"`] : []),
170
+ "---",
171
+ "```",
172
+ "",
173
+ "After the frontmatter, write the chapter body starting with the first `## Section Title` heading.",
174
+ );
175
+
176
+ return sections.join("\n");
177
+ }
178
+
179
+ /** Map chapter ID to markdown filename slug */
180
+ function chapterIdToSlug(chapterId: string): string {
181
+ const slugMap: Record<string, string> = {
182
+ "ch-1": "ch-1-project-management",
183
+ "ch-2": "ch-2-task-execution",
184
+ "ch-3": "ch-3-document-processing",
185
+ "ch-4": "ch-4-workflow-orchestration",
186
+ "ch-5": "ch-5-scheduled-intelligence",
187
+ "ch-6": "ch-6-agent-self-improvement",
188
+ "ch-7": "ch-7-multi-agent-swarms",
189
+ "ch-8": "ch-8-human-in-the-loop",
190
+ "ch-9": "ch-9-autonomous-organization",
191
+ };
192
+ return slugMap[chapterId] ?? chapterId;
193
+ }