stagent 0.6.2 → 0.6.3

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 (68) hide show
  1. package/dist/cli.js +47 -1
  2. package/package.json +1 -2
  3. package/src/app/api/documents/[id]/route.ts +5 -1
  4. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  5. package/src/app/api/documents/route.ts +5 -1
  6. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  7. package/src/app/api/projects/[id]/route.ts +35 -3
  8. package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
  9. package/src/app/api/schedules/route.ts +19 -1
  10. package/src/app/api/tasks/[id]/route.ts +37 -2
  11. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  12. package/src/app/api/tasks/route.ts +8 -9
  13. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  14. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  15. package/src/app/api/workflows/[id]/route.ts +16 -3
  16. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  17. package/src/app/api/workflows/route.ts +13 -2
  18. package/src/app/documents/page.tsx +5 -1
  19. package/src/app/layout.tsx +0 -1
  20. package/src/app/manifest.ts +3 -3
  21. package/src/app/projects/[id]/page.tsx +62 -2
  22. package/src/components/documents/document-chip-bar.tsx +17 -1
  23. package/src/components/documents/document-detail-view.tsx +51 -0
  24. package/src/components/documents/document-grid.tsx +5 -0
  25. package/src/components/documents/document-table.tsx +4 -0
  26. package/src/components/documents/types.ts +3 -0
  27. package/src/components/projects/project-form-sheet.tsx +133 -2
  28. package/src/components/schedules/schedule-form.tsx +113 -1
  29. package/src/components/shared/document-picker-sheet.tsx +283 -0
  30. package/src/components/tasks/task-card.tsx +8 -1
  31. package/src/components/tasks/task-create-panel.tsx +137 -14
  32. package/src/components/tasks/task-detail-view.tsx +47 -0
  33. package/src/components/tasks/task-edit-dialog.tsx +125 -2
  34. package/src/components/workflows/workflow-form-view.tsx +231 -7
  35. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  36. package/src/components/workflows/workflow-list.tsx +90 -45
  37. package/src/components/workflows/workflow-status-view.tsx +167 -22
  38. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  39. package/src/lib/agents/profiles/registry.ts +6 -3
  40. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  41. package/src/lib/book/chapter-generator.ts +4 -19
  42. package/src/lib/book/chapter-mapping.ts +17 -0
  43. package/src/lib/book/content.ts +5 -16
  44. package/src/lib/book/update-detector.ts +3 -16
  45. package/src/lib/chat/engine.ts +1 -0
  46. package/src/lib/chat/system-prompt.ts +9 -1
  47. package/src/lib/chat/tool-catalog.ts +1 -0
  48. package/src/lib/chat/tools/settings-tools.ts +109 -0
  49. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  50. package/src/lib/data/clear.ts +12 -0
  51. package/src/lib/db/bootstrap.ts +48 -0
  52. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  53. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  54. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  55. package/src/lib/db/schema.ts +77 -0
  56. package/src/lib/docs/reader.ts +2 -3
  57. package/src/lib/documents/context-builder.ts +75 -2
  58. package/src/lib/documents/document-resolver.ts +119 -0
  59. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  60. package/src/lib/schedules/scheduler.ts +31 -1
  61. package/src/lib/utils/app-root.ts +20 -0
  62. package/src/lib/validators/__tests__/task.test.ts +43 -10
  63. package/src/lib/validators/task.ts +7 -1
  64. package/src/lib/workflows/blueprints/registry.ts +3 -3
  65. package/src/lib/workflows/engine.ts +24 -8
  66. package/src/lib/workflows/types.ts +14 -0
  67. package/public/icon.svg +0 -13
  68. package/src/components/tasks/file-upload.tsx +0 -120
@@ -2,6 +2,76 @@ import { defineTool } from "../tool-registry";
2
2
  import { z } from "zod";
3
3
  import { ok, err, type ToolContext } from "./helpers";
4
4
 
5
+ /* ── Writable settings allowlist ─────────────────────────────────── */
6
+
7
+ interface WritableSetting {
8
+ validate: (value: string) => string | null; // error message or null
9
+ description: string;
10
+ }
11
+
12
+ const WRITABLE_SETTINGS: Record<string, WritableSetting> = {
13
+ "runtime.sdkTimeoutSeconds": {
14
+ description: "SDK timeout in seconds (10–300)",
15
+ validate: (v) => {
16
+ const n = parseInt(v, 10);
17
+ return isNaN(n) || n < 10 || n > 300 ? "Must be integer 10–300" : null;
18
+ },
19
+ },
20
+ "runtime.maxTurns": {
21
+ description: "Max agent turns per task (1–50)",
22
+ validate: (v) => {
23
+ const n = parseInt(v, 10);
24
+ return isNaN(n) || n < 1 || n > 50 ? "Must be integer 1–50" : null;
25
+ },
26
+ },
27
+ "routing.preference": {
28
+ description: "Routing preference: cost | latency | quality | manual",
29
+ validate: (v) =>
30
+ ["cost", "latency", "quality", "manual"].includes(v)
31
+ ? null
32
+ : "Must be one of: cost, latency, quality, manual",
33
+ },
34
+ "browser.chromeDevtoolsEnabled": {
35
+ description: "Enable Chrome DevTools MCP: true | false",
36
+ validate: (v) =>
37
+ ["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
38
+ },
39
+ "browser.playwrightEnabled": {
40
+ description: "Enable Playwright MCP: true | false",
41
+ validate: (v) =>
42
+ ["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
43
+ },
44
+ "web.exaSearchEnabled": {
45
+ description: "Enable Exa web search: true | false",
46
+ validate: (v) =>
47
+ ["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
48
+ },
49
+ "learning.contextCharLimit": {
50
+ description: "Learning context char limit (2000–32000, step 1000)",
51
+ validate: (v) => {
52
+ const n = parseInt(v, 10);
53
+ return isNaN(n) || n < 2000 || n > 32000 || n % 1000 !== 0
54
+ ? "Must be integer 2000–32000, step 1000"
55
+ : null;
56
+ },
57
+ },
58
+ "ollama.baseUrl": {
59
+ description: "Ollama server base URL",
60
+ validate: (v) => (v.trim().length === 0 ? "Must be non-empty URL" : null),
61
+ },
62
+ "ollama.defaultModel": {
63
+ description: "Default Ollama model name",
64
+ validate: (v) =>
65
+ v.trim().length === 0 ? "Must be non-empty string" : null,
66
+ },
67
+ };
68
+
69
+ const WRITABLE_KEYS_DOC = Object.entries(WRITABLE_SETTINGS)
70
+ .map(([k, v]) => `- "${k}": ${v.description}`)
71
+ .join("\n");
72
+
73
+ /* ── Tool definitions ────────────────────────────────────────────── */
74
+
5
75
  export function settingsTools(_ctx: ToolContext) {
6
76
  return [
7
77
  defineTool(
@@ -69,5 +139,44 @@ export function settingsTools(_ctx: ToolContext) {
69
139
  }
70
140
  }
71
141
  ),
142
+
143
+ defineTool(
144
+ "set_settings",
145
+ `Update a Stagent setting. Requires user approval.\n\nWritable keys:\n${WRITABLE_KEYS_DOC}`,
146
+ {
147
+ key: z.string().describe("Setting key to update"),
148
+ value: z.string().describe("New value (always a string)"),
149
+ },
150
+ async (args) => {
151
+ const spec = WRITABLE_SETTINGS[args.key];
152
+ if (!spec) {
153
+ return err(
154
+ `Key "${args.key}" is not writable. Valid keys: ${Object.keys(WRITABLE_SETTINGS).join(", ")}`
155
+ );
156
+ }
157
+ const validationError = spec.validate(args.value);
158
+ if (validationError) {
159
+ return err(
160
+ `Invalid value for "${args.key}": ${validationError}`
161
+ );
162
+ }
163
+ try {
164
+ const { getSetting, setSetting } = await import(
165
+ "@/lib/settings/helpers"
166
+ );
167
+ const oldValue = await getSetting(args.key);
168
+ await setSetting(args.key, args.value);
169
+ return ok({
170
+ key: args.key,
171
+ oldValue: oldValue ?? "(unset)",
172
+ newValue: args.value,
173
+ });
174
+ } catch (e) {
175
+ return err(
176
+ e instanceof Error ? e.message : "Failed to update setting"
177
+ );
178
+ }
179
+ }
180
+ ),
72
181
  ];
73
182
  }
@@ -1,8 +1,15 @@
1
1
  import { defineTool } from "../tool-registry";
2
2
  import { z } from "zod";
3
3
  import { db } from "@/lib/db";
4
- import { workflows, tasks, agentLogs, notifications, documents } from "@/lib/db/schema";
5
- import { eq, and, desc } from "drizzle-orm";
4
+ import {
5
+ workflows,
6
+ tasks,
7
+ agentLogs,
8
+ notifications,
9
+ documents,
10
+ workflowDocumentInputs,
11
+ } from "@/lib/db/schema";
12
+ import { eq, and, desc, inArray, like } from "drizzle-orm";
6
13
  import { ok, err, type ToolContext } from "./helpers";
7
14
 
8
15
  const VALID_WORKFLOW_STATUSES = [
@@ -74,6 +81,12 @@ export function workflowTools(ctx: ToolContext) {
74
81
  .describe(
75
82
  'Workflow definition as JSON string. Must include "pattern" and "steps" array. Example: {"pattern":"sequence","steps":[{"name":"step1","prompt":"Do X","assignedAgent":"claude"}]}'
76
83
  ),
84
+ documentIds: z
85
+ .array(z.string())
86
+ .optional()
87
+ .describe(
88
+ "Optional array of document IDs from the project pool to attach as input context. These documents will be injected into all workflow steps at execution time."
89
+ ),
77
90
  },
78
91
  async (args) => {
79
92
  try {
@@ -103,6 +116,25 @@ export function workflowTools(ctx: ToolContext) {
103
116
  updatedAt: now,
104
117
  });
105
118
 
119
+ // Attach pool documents if provided
120
+ const attachedDocs: string[] = [];
121
+ if (args.documentIds && args.documentIds.length > 0) {
122
+ for (const docId of args.documentIds) {
123
+ try {
124
+ await db.insert(workflowDocumentInputs).values({
125
+ id: crypto.randomUUID(),
126
+ workflowId: id,
127
+ documentId: docId,
128
+ stepId: null,
129
+ createdAt: now,
130
+ });
131
+ attachedDocs.push(docId);
132
+ } catch {
133
+ // Skip duplicates or invalid doc IDs
134
+ }
135
+ }
136
+ }
137
+
106
138
  const [workflow] = await db
107
139
  .select()
108
140
  .from(workflows)
@@ -115,6 +147,7 @@ export function workflowTools(ctx: ToolContext) {
115
147
  projectId: workflow.projectId,
116
148
  status: workflow.status,
117
149
  createdAt: workflow.createdAt,
150
+ attachedDocuments: attachedDocs.length,
118
151
  });
119
152
  } catch (e) {
120
153
  return err(e instanceof Error ? e.message : "Failed to create workflow");
@@ -354,5 +387,115 @@ export function workflowTools(ctx: ToolContext) {
354
387
  }
355
388
  }
356
389
  ),
390
+
391
+ defineTool(
392
+ "find_related_documents",
393
+ "Search for documents in the project pool that could be used as context for a workflow. Returns output documents from completed workflows and uploaded documents. Use this proactively when creating follow-up workflows to discover relevant context.",
394
+ {
395
+ projectId: z
396
+ .string()
397
+ .optional()
398
+ .describe("Project ID to search in. Omit to use the active project."),
399
+ query: z
400
+ .string()
401
+ .optional()
402
+ .describe("Search query to match against document names"),
403
+ direction: z
404
+ .enum(["input", "output"])
405
+ .optional()
406
+ .describe('Filter by direction. Use "output" to find documents produced by other workflows.'),
407
+ sourceWorkflowId: z
408
+ .string()
409
+ .optional()
410
+ .describe("Filter to documents produced by a specific workflow"),
411
+ limit: z
412
+ .number()
413
+ .optional()
414
+ .describe("Maximum number of documents to return (default: 20)"),
415
+ },
416
+ async (args) => {
417
+ try {
418
+ const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
419
+ if (!effectiveProjectId) {
420
+ return err("No project context — specify a projectId or set an active project");
421
+ }
422
+
423
+ const conditions = [
424
+ eq(documents.projectId, effectiveProjectId),
425
+ eq(documents.status, "ready"),
426
+ ];
427
+
428
+ if (args.direction) {
429
+ conditions.push(eq(documents.direction, args.direction));
430
+ }
431
+
432
+ if (args.query) {
433
+ conditions.push(like(documents.originalName, `%${args.query}%`));
434
+ }
435
+
436
+ if (args.sourceWorkflowId) {
437
+ // Find task IDs belonging to the source workflow
438
+ const workflowTasks = await db
439
+ .select({ id: tasks.id })
440
+ .from(tasks)
441
+ .where(eq(tasks.workflowId, args.sourceWorkflowId));
442
+
443
+ const taskIds = workflowTasks.map((t) => t.id);
444
+ if (taskIds.length > 0) {
445
+ conditions.push(inArray(documents.taskId, taskIds));
446
+ } else {
447
+ return ok([]); // No tasks for this workflow
448
+ }
449
+ }
450
+
451
+ const result = await db
452
+ .select({
453
+ id: documents.id,
454
+ originalName: documents.originalName,
455
+ mimeType: documents.mimeType,
456
+ size: documents.size,
457
+ direction: documents.direction,
458
+ category: documents.category,
459
+ status: documents.status,
460
+ taskId: documents.taskId,
461
+ createdAt: documents.createdAt,
462
+ })
463
+ .from(documents)
464
+ .where(and(...conditions))
465
+ .orderBy(desc(documents.createdAt))
466
+ .limit(args.limit ?? 20);
467
+
468
+ // Enrich with source workflow name
469
+ const enriched = await Promise.all(
470
+ result.map(async (doc) => {
471
+ let sourceWorkflowName: string | null = null;
472
+ if (doc.taskId) {
473
+ const [task] = await db
474
+ .select({ workflowId: tasks.workflowId })
475
+ .from(tasks)
476
+ .where(eq(tasks.id, doc.taskId));
477
+ if (task?.workflowId) {
478
+ const [wf] = await db
479
+ .select({ name: workflows.name })
480
+ .from(workflows)
481
+ .where(eq(workflows.id, task.workflowId));
482
+ sourceWorkflowName = wf?.name ?? null;
483
+ }
484
+ }
485
+ return {
486
+ ...doc,
487
+ sourceWorkflow: sourceWorkflowName,
488
+ };
489
+ })
490
+ );
491
+
492
+ return ok(enriched);
493
+ } catch (e) {
494
+ return err(
495
+ e instanceof Error ? e.message : "Failed to find documents"
496
+ );
497
+ }
498
+ }
499
+ ),
357
500
  ];
358
501
  }
@@ -25,6 +25,9 @@ import {
25
25
  channelBindings,
26
26
  channelConfigs,
27
27
  agentMessages,
28
+ workflowDocumentInputs,
29
+ scheduleDocumentInputs,
30
+ projectDocumentDefaults,
28
31
  } from "@/lib/db/schema";
29
32
  import { readdirSync, unlinkSync, mkdirSync } from "fs";
30
33
  import { join } from "path";
@@ -71,6 +74,12 @@ export function clearAllData() {
71
74
  const usageLedgerDeleted = db.delete(usageLedger).run().changes;
72
75
  const logsDeleted = db.delete(agentLogs).run().changes;
73
76
  const notificationsDeleted = db.delete(notifications).run().changes;
77
+
78
+ // Document junction tables — delete before documents, workflows, schedules, projects
79
+ const workflowDocInputsDeleted = db.delete(workflowDocumentInputs).run().changes;
80
+ const scheduleDocInputsDeleted = db.delete(scheduleDocumentInputs).run().changes;
81
+ const projectDocDefaultsDeleted = db.delete(projectDocumentDefaults).run().changes;
82
+
74
83
  const documentsDeleted = db.delete(documents).run().changes;
75
84
  const agentMemoryDeleted = db.delete(agentMemory).run().changes;
76
85
  const learnedContextDeleted = db.delete(learnedContext).run().changes;
@@ -130,6 +139,9 @@ export function clearAllData() {
130
139
  agentMessages: agentMessagesDeleted,
131
140
  channelBindings: channelBindingsDeleted,
132
141
  channelConfigs: channelConfigsDeleted,
142
+ workflowDocumentInputs: workflowDocInputsDeleted,
143
+ scheduleDocumentInputs: scheduleDocInputsDeleted,
144
+ projectDocumentDefaults: projectDocDefaultsDeleted,
133
145
  files: filesDeleted,
134
146
  screenshots: screenshotsDeleted,
135
147
  };
@@ -28,6 +28,9 @@ const STAGENT_TABLES = [
28
28
  "channel_configs",
29
29
  "channel_bindings",
30
30
  "agent_messages",
31
+ "workflow_document_inputs",
32
+ "schedule_document_inputs",
33
+ "project_document_defaults",
31
34
  ] as const;
32
35
 
33
36
  export function bootstrapStagentDatabase(sqlite: Database.Database): void {
@@ -55,6 +58,7 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
55
58
  result TEXT,
56
59
  session_id TEXT,
57
60
  resume_count INTEGER DEFAULT 0 NOT NULL,
61
+ workflow_run_number INTEGER,
58
62
  created_at INTEGER NOT NULL,
59
63
  updated_at INTEGER NOT NULL,
60
64
  FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
@@ -68,6 +72,7 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
68
72
  name TEXT NOT NULL,
69
73
  definition TEXT NOT NULL,
70
74
  status TEXT DEFAULT 'draft' NOT NULL,
75
+ run_number INTEGER DEFAULT 0 NOT NULL,
71
76
  created_at INTEGER NOT NULL,
72
77
  updated_at INTEGER NOT NULL,
73
78
  FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
@@ -272,6 +277,8 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
272
277
 
273
278
  // Task source type
274
279
  addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN source_type TEXT;`);
280
+ addColumnIfMissing(`ALTER TABLE workflows ADD COLUMN run_number INTEGER DEFAULT 0 NOT NULL;`);
281
+ addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN workflow_run_number INTEGER;`);
275
282
  addColumnIfMissing(`ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;`);
276
283
  addColumnIfMissing(`ALTER TABLE documents ADD COLUMN source TEXT DEFAULT 'upload';`);
277
284
  addColumnIfMissing(`ALTER TABLE documents ADD COLUMN conversation_id TEXT REFERENCES conversations(id);`);
@@ -566,6 +573,47 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
566
573
  CREATE INDEX IF NOT EXISTS idx_agent_messages_to_status ON agent_messages(to_profile_id, status);
567
574
  CREATE INDEX IF NOT EXISTS idx_agent_messages_task ON agent_messages(task_id);
568
575
  `);
576
+
577
+ // ── Workflow Document Pool ──────────────────────────────────────────
578
+ sqlite.exec(`
579
+ CREATE TABLE IF NOT EXISTS workflow_document_inputs (
580
+ id TEXT PRIMARY KEY NOT NULL,
581
+ workflow_id TEXT NOT NULL,
582
+ document_id TEXT NOT NULL,
583
+ step_id TEXT,
584
+ created_at INTEGER NOT NULL,
585
+ FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
586
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
587
+ );
588
+
589
+ CREATE INDEX IF NOT EXISTS idx_wdi_workflow ON workflow_document_inputs(workflow_id);
590
+ CREATE INDEX IF NOT EXISTS idx_wdi_document ON workflow_document_inputs(document_id);
591
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_wdi_workflow_doc_step ON workflow_document_inputs(workflow_id, document_id, step_id);
592
+
593
+ CREATE TABLE IF NOT EXISTS schedule_document_inputs (
594
+ id TEXT PRIMARY KEY NOT NULL,
595
+ schedule_id TEXT NOT NULL,
596
+ document_id TEXT NOT NULL,
597
+ created_at INTEGER NOT NULL,
598
+ FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
599
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
600
+ );
601
+
602
+ CREATE INDEX IF NOT EXISTS idx_sdi_schedule ON schedule_document_inputs(schedule_id);
603
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sdi_schedule_doc ON schedule_document_inputs(schedule_id, document_id);
604
+
605
+ CREATE TABLE IF NOT EXISTS project_document_defaults (
606
+ id TEXT PRIMARY KEY NOT NULL,
607
+ project_id TEXT NOT NULL,
608
+ document_id TEXT NOT NULL,
609
+ created_at INTEGER NOT NULL,
610
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
611
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
612
+ );
613
+
614
+ CREATE INDEX IF NOT EXISTS idx_pdd_project ON project_document_defaults(project_id);
615
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_pdd_project_doc ON project_document_defaults(project_id, document_id);
616
+ `);
569
617
  }
570
618
 
571
619
  export function hasLegacyStagentTables(sqlite: Database.Database): boolean {
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS workflow_document_inputs (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ workflow_id TEXT NOT NULL,
4
+ document_id TEXT NOT NULL,
5
+ step_id TEXT,
6
+ created_at INTEGER NOT NULL,
7
+ FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
8
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
9
+ );
10
+
11
+ CREATE INDEX IF NOT EXISTS idx_wdi_workflow ON workflow_document_inputs(workflow_id);
12
+ CREATE INDEX IF NOT EXISTS idx_wdi_document ON workflow_document_inputs(document_id);
13
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_wdi_workflow_doc_step ON workflow_document_inputs(workflow_id, document_id, step_id);
@@ -0,0 +1,25 @@
1
+ -- Schedule document inputs (context documents for scheduled tasks)
2
+ CREATE TABLE IF NOT EXISTS `schedule_document_inputs` (
3
+ `id` text PRIMARY KEY NOT NULL,
4
+ `schedule_id` text NOT NULL,
5
+ `document_id` text NOT NULL,
6
+ `created_at` integer NOT NULL,
7
+ FOREIGN KEY (`schedule_id`) REFERENCES `schedules`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
8
+ FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
9
+ );
10
+
11
+ CREATE INDEX IF NOT EXISTS `idx_sdi_schedule` ON `schedule_document_inputs` (`schedule_id`);
12
+ CREATE UNIQUE INDEX IF NOT EXISTS `idx_sdi_schedule_doc` ON `schedule_document_inputs` (`schedule_id`, `document_id`);
13
+
14
+ -- Project document defaults (auto-attached to new tasks/workflows in project)
15
+ CREATE TABLE IF NOT EXISTS `project_document_defaults` (
16
+ `id` text PRIMARY KEY NOT NULL,
17
+ `project_id` text NOT NULL,
18
+ `document_id` text NOT NULL,
19
+ `created_at` integer NOT NULL,
20
+ FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
21
+ FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS `idx_pdd_project` ON `project_document_defaults` (`project_id`);
25
+ CREATE UNIQUE INDEX IF NOT EXISTS `idx_pdd_project_doc` ON `project_document_defaults` (`project_id`, `document_id`);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE workflows ADD COLUMN run_number INTEGER DEFAULT 0 NOT NULL;
2
+ ALTER TABLE tasks ADD COLUMN workflow_run_number INTEGER;
@@ -37,6 +37,7 @@ export const tasks = sqliteTable(
37
37
  sourceType: text("source_type", {
38
38
  enum: ["manual", "scheduled", "heartbeat", "workflow"],
39
39
  }),
40
+ workflowRunNumber: integer("workflow_run_number"),
40
41
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
41
42
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
42
43
  },
@@ -59,6 +60,7 @@ export const workflows = sqliteTable("workflows", {
59
60
  })
60
61
  .default("draft")
61
62
  .notNull(),
63
+ runNumber: integer("run_number").default(0).notNull(),
62
64
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
63
65
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
64
66
  });
@@ -678,6 +680,81 @@ export const agentMessages = sqliteTable(
678
680
  ]
679
681
  );
680
682
 
683
+ // ── Workflow Document Pool ───────────────────────────────────────────
684
+
685
+ export const workflowDocumentInputs = sqliteTable(
686
+ "workflow_document_inputs",
687
+ {
688
+ id: text("id").primaryKey(),
689
+ workflowId: text("workflow_id")
690
+ .references(() => workflows.id)
691
+ .notNull(),
692
+ documentId: text("document_id")
693
+ .references(() => documents.id)
694
+ .notNull(),
695
+ /** null = document available to all steps; set = scoped to specific step */
696
+ stepId: text("step_id"),
697
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
698
+ },
699
+ (table) => [
700
+ index("idx_wdi_workflow").on(table.workflowId),
701
+ index("idx_wdi_document").on(table.documentId),
702
+ uniqueIndex("idx_wdi_workflow_doc_step").on(
703
+ table.workflowId,
704
+ table.documentId,
705
+ table.stepId
706
+ ),
707
+ ]
708
+ );
709
+
710
+ export type WorkflowDocumentInputRow = InferSelectModel<typeof workflowDocumentInputs>;
711
+
712
+ export const scheduleDocumentInputs = sqliteTable(
713
+ "schedule_document_inputs",
714
+ {
715
+ id: text("id").primaryKey(),
716
+ scheduleId: text("schedule_id")
717
+ .references(() => schedules.id)
718
+ .notNull(),
719
+ documentId: text("document_id")
720
+ .references(() => documents.id)
721
+ .notNull(),
722
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
723
+ },
724
+ (table) => [
725
+ index("idx_sdi_schedule").on(table.scheduleId),
726
+ uniqueIndex("idx_sdi_schedule_doc").on(
727
+ table.scheduleId,
728
+ table.documentId
729
+ ),
730
+ ]
731
+ );
732
+
733
+ export type ScheduleDocumentInputRow = InferSelectModel<typeof scheduleDocumentInputs>;
734
+
735
+ export const projectDocumentDefaults = sqliteTable(
736
+ "project_document_defaults",
737
+ {
738
+ id: text("id").primaryKey(),
739
+ projectId: text("project_id")
740
+ .references(() => projects.id)
741
+ .notNull(),
742
+ documentId: text("document_id")
743
+ .references(() => documents.id)
744
+ .notNull(),
745
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
746
+ },
747
+ (table) => [
748
+ index("idx_pdd_project").on(table.projectId),
749
+ uniqueIndex("idx_pdd_project_doc").on(
750
+ table.projectId,
751
+ table.documentId
752
+ ),
753
+ ]
754
+ );
755
+
756
+ export type ProjectDocumentDefaultRow = InferSelectModel<typeof projectDocumentDefaults>;
757
+
681
758
  // Shared types derived from schema — use these in components instead of `as any`
682
759
  export type ProjectRow = InferSelectModel<typeof projects>;
683
760
  export type TaskRow = InferSelectModel<typeof tasks>;
@@ -1,12 +1,11 @@
1
1
  import { readFileSync, readdirSync, existsSync } from "fs";
2
2
  import { join, basename } from "path";
3
3
  import type { DocManifest, ParsedDoc } from "./types";
4
+ import { getAppRoot } from "../utils/app-root";
4
5
 
5
6
  /** Resolve the docs directory relative to this source file (npx-safe) */
6
7
  function docsDir(): string {
7
- const dir = import.meta.dirname ?? __dirname;
8
- // src/lib/docs/ → project root → docs/
9
- return join(dir, "..", "..", "..", "docs");
8
+ return join(getAppRoot(import.meta.dirname, 3), "docs");
10
9
  }
11
10
 
12
11
  /** Read and parse docs/manifest.json */
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { db } from "@/lib/db";
7
- import { documents } from "@/lib/db/schema";
8
- import { and, eq } from "drizzle-orm";
7
+ import { documents, workflowDocumentInputs } from "@/lib/db/schema";
8
+ import { and, eq, inArray, isNull } from "drizzle-orm";
9
9
  import type { DocumentRow } from "@/lib/db/schema";
10
10
 
11
11
  const MAX_INLINE_TEXT = 10_000;
@@ -114,3 +114,76 @@ export async function buildWorkflowDocumentContext(
114
114
  return null;
115
115
  }
116
116
  }
117
+
118
+ /**
119
+ * Build document context from the workflow document pool (junction table).
120
+ * Queries workflow_document_inputs for documents bound to this workflow,
121
+ * optionally scoped to a specific step. Returns null if no pool documents.
122
+ *
123
+ * Includes both workflow-level bindings (stepId=null) and step-specific bindings.
124
+ */
125
+ export async function buildPoolDocumentContext(
126
+ workflowId: string,
127
+ stepId?: string
128
+ ): Promise<string | null> {
129
+ try {
130
+ // Get workflow-level (stepId=null) bindings — available to all steps
131
+ const globalBindings = await db
132
+ .select({ documentId: workflowDocumentInputs.documentId })
133
+ .from(workflowDocumentInputs)
134
+ .where(
135
+ and(
136
+ eq(workflowDocumentInputs.workflowId, workflowId),
137
+ isNull(workflowDocumentInputs.stepId)
138
+ )
139
+ );
140
+
141
+ // If a specific step, also get step-scoped bindings
142
+ let stepBindings: { documentId: string }[] = [];
143
+ if (stepId) {
144
+ stepBindings = await db
145
+ .select({ documentId: workflowDocumentInputs.documentId })
146
+ .from(workflowDocumentInputs)
147
+ .where(
148
+ and(
149
+ eq(workflowDocumentInputs.workflowId, workflowId),
150
+ eq(workflowDocumentInputs.stepId, stepId)
151
+ )
152
+ );
153
+ }
154
+
155
+ // Deduplicate document IDs
156
+ const docIdSet = new Set<string>();
157
+ for (const b of [...globalBindings, ...stepBindings]) {
158
+ docIdSet.add(b.documentId);
159
+ }
160
+
161
+ if (docIdSet.size === 0) return null;
162
+
163
+ const docs = await db
164
+ .select()
165
+ .from(documents)
166
+ .where(inArray(documents.id, [...docIdSet]));
167
+
168
+ if (docs.length === 0) return null;
169
+
170
+ const sections = docs.map((doc, i) => formatDocument(doc, i));
171
+ let result = sections.join("\n\n");
172
+
173
+ if (result.length > MAX_WORKFLOW_DOC_CONTEXT) {
174
+ result = result.slice(0, MAX_WORKFLOW_DOC_CONTEXT);
175
+ result += `\n\n(Pool document context truncated at ${MAX_WORKFLOW_DOC_CONTEXT} chars — use Read tool for full content)`;
176
+ }
177
+
178
+ return [
179
+ "--- Workflow Pool Documents ---",
180
+ "",
181
+ result,
182
+ "",
183
+ "--- End Workflow Pool Documents ---",
184
+ ].join("\n");
185
+ } catch (error) {
186
+ console.error("[context-builder] Failed to build pool document context:", error);
187
+ return null;
188
+ }
189
+ }