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
@@ -23,7 +23,10 @@ import {
23
23
  MessageSquareMore,
24
24
  FileText,
25
25
  Paperclip,
26
+ ArrowRight,
27
+ FolderKanban,
26
28
  } from "lucide-react";
29
+ import { Checkbox } from "@/components/ui/checkbox";
27
30
  import ReactMarkdown from "react-markdown";
28
31
  import remarkGfm from "remark-gfm";
29
32
  import { toast } from "sonner";
@@ -72,6 +75,13 @@ interface WorkflowStatusData {
72
75
  swarmConfig?: SwarmConfig;
73
76
  stepDocuments?: Record<string, DocumentInfo[]>;
74
77
  parentDocuments?: DocumentInfo[];
78
+ runNumber?: number;
79
+ runHistory?: Array<{
80
+ runNumber: number | null;
81
+ taskCount: number;
82
+ completedCount: number;
83
+ failedCount: number;
84
+ }>;
75
85
  }
76
86
 
77
87
  interface WorkflowStatusViewProps {
@@ -315,6 +325,23 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
315
325
  <p className="text-sm text-muted-foreground mt-1">
316
326
  {patternLabels[data.pattern] ?? data.pattern}
317
327
  </p>
328
+ <div className="flex items-center gap-2 mt-1">
329
+ {data.projectId && (
330
+ <Badge
331
+ variant="outline"
332
+ className="text-xs cursor-pointer hover:bg-accent gap-1"
333
+ onClick={() => router.push(`/projects/${data.projectId}`)}
334
+ >
335
+ <FolderKanban className="h-3 w-3" />
336
+ Project
337
+ </Badge>
338
+ )}
339
+ {data.runNumber != null && data.runNumber > 0 && (
340
+ <Badge variant="outline" className="text-xs font-normal">
341
+ Run #{data.runNumber}
342
+ </Badge>
343
+ )}
344
+ </div>
318
345
  </div>
319
346
  </div>
320
347
  <div className="flex items-center gap-2">
@@ -330,56 +357,53 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
330
357
  </Button>
331
358
  )}
332
359
 
333
- {/* Edit — draft only */}
334
- {data.status === "draft" && hasDefinition && (
360
+ {/* Edit — draft, completed, or failed */}
361
+ {["draft", "completed", "failed"].includes(data.status) && hasDefinition && (
335
362
  <Button
336
- variant="ghost"
337
- size="icon"
338
- className="h-7 w-7"
339
- aria-label="Edit workflow"
363
+ variant="outline"
364
+ size="sm"
340
365
  onClick={() => router.push(`/workflows/${workflowId}/edit`)}
341
366
  >
342
- <Pencil className="h-3.5 w-3.5" />
367
+ <Pencil className="h-3.5 w-3.5 mr-1.5" />
368
+ Edit
343
369
  </Button>
344
370
  )}
345
371
 
346
372
  {/* Clone — always available */}
347
373
  {hasDefinition && (
348
374
  <Button
349
- variant="ghost"
350
- size="icon"
351
- className="h-7 w-7"
352
- aria-label="Clone workflow"
375
+ variant="outline"
376
+ size="sm"
353
377
  onClick={() => router.push(`/workflows/${workflowId}/edit?clone=true`)}
354
378
  >
355
- <Copy className="h-3.5 w-3.5" />
379
+ <Copy className="h-3.5 w-3.5 mr-1.5" />
380
+ Clone
356
381
  </Button>
357
382
  )}
358
383
 
359
384
  {/* Re-run — completed/failed only */}
360
385
  {(data.status === "completed" || data.status === "failed") && (
361
386
  <Button
362
- variant="ghost"
363
- size="icon"
364
- className="h-7 w-7"
365
- aria-label="Re-run workflow"
387
+ variant="outline"
388
+ size="sm"
366
389
  onClick={handleRerun}
367
390
  disabled={executing}
368
391
  >
369
- <RotateCcw className="h-3.5 w-3.5" />
392
+ <RotateCcw className="h-3.5 w-3.5 mr-1.5" />
393
+ Re-run
370
394
  </Button>
371
395
  )}
372
396
 
373
397
  {/* Delete — not active */}
374
398
  {data.status !== "active" && (
375
399
  <Button
376
- variant="ghost"
377
- size="icon"
378
- className="h-7 w-7 text-destructive"
379
- aria-label="Delete workflow"
400
+ variant="outline"
401
+ size="sm"
402
+ className="text-destructive hover:text-destructive"
380
403
  onClick={() => setConfirmDelete(true)}
381
404
  >
382
- <Trash2 className="h-3.5 w-3.5" />
405
+ <Trash2 className="h-3.5 w-3.5 mr-1.5" />
406
+ Delete
383
407
  </Button>
384
408
  )}
385
409
  </div>
@@ -614,6 +638,11 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
614
638
  />
615
639
  )}
616
640
 
641
+ {/* Output Dock — chain into new workflow */}
642
+ {data.status === "completed" && hasStepDocs && (
643
+ <OutputDock stepDocuments={data.stepDocuments!} steps={data.steps} />
644
+ )}
645
+
617
646
  {/* Delete confirmation */}
618
647
  <ConfirmDialog
619
648
  open={confirmDelete}
@@ -627,3 +656,119 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
627
656
  </div>
628
657
  );
629
658
  }
659
+
660
+ /** Output Dock — selectable output documents for chaining into a new workflow */
661
+ function OutputDock({
662
+ stepDocuments,
663
+ steps,
664
+ }: {
665
+ stepDocuments: Record<string, DocumentInfo[]>;
666
+ steps: StepWithState[];
667
+ }) {
668
+ const router = useRouter();
669
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
670
+
671
+ // Flatten all output documents
672
+ const allOutputDocs = Object.entries(stepDocuments).flatMap(
673
+ ([taskId, docs]) => {
674
+ const step = steps.find((s) => s.state.taskId === taskId);
675
+ return docs.map((doc) => ({
676
+ ...doc,
677
+ stepName: step?.name ?? "Unknown Step",
678
+ }));
679
+ }
680
+ );
681
+
682
+ if (allOutputDocs.length === 0) return null;
683
+
684
+ function toggleDoc(id: string) {
685
+ setSelectedIds((prev) => {
686
+ const next = new Set(prev);
687
+ if (next.has(id)) next.delete(id);
688
+ else next.add(id);
689
+ return next;
690
+ });
691
+ }
692
+
693
+ function selectAll() {
694
+ setSelectedIds(new Set(allOutputDocs.map((d) => d.id)));
695
+ }
696
+
697
+ function chainIntoNewWorkflow() {
698
+ if (selectedIds.size === 0) return;
699
+ const params = new URLSearchParams({
700
+ inputDocs: [...selectedIds].join(","),
701
+ });
702
+ router.push(`/workflows/new?${params}`);
703
+ }
704
+
705
+ return (
706
+ <Card>
707
+ <CardHeader className="pb-3">
708
+ <div className="flex items-center justify-between">
709
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
710
+ <ArrowRight className="h-4 w-4" />
711
+ Chain Output Documents
712
+ </CardTitle>
713
+ <Button
714
+ variant="ghost"
715
+ size="sm"
716
+ onClick={selectAll}
717
+ className="text-xs"
718
+ >
719
+ Select All
720
+ </Button>
721
+ </div>
722
+ <p className="text-xs text-muted-foreground">
723
+ Select output documents to use as inputs in a new workflow
724
+ </p>
725
+ </CardHeader>
726
+ <CardContent>
727
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mb-4">
728
+ {allOutputDocs.map((doc) => {
729
+ const isChecked = selectedIds.has(doc.id);
730
+ return (
731
+ <div
732
+ key={doc.id}
733
+ role="button"
734
+ tabIndex={0}
735
+ onClick={() => toggleDoc(doc.id)}
736
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleDoc(doc.id); } }}
737
+ className={`flex items-center gap-3 p-3 rounded-lg text-left transition-colors border cursor-pointer ${
738
+ isChecked
739
+ ? "bg-accent/50 border-accent"
740
+ : "hover:bg-muted/50 border-border/50"
741
+ }`}
742
+ >
743
+ <Checkbox
744
+ checked={isChecked}
745
+ onCheckedChange={() => toggleDoc(doc.id)}
746
+ />
747
+ <FileText className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
748
+ <div className="flex-1 min-w-0">
749
+ <p className="text-sm font-medium truncate">
750
+ {doc.originalName}
751
+ </p>
752
+ <p className="text-xs text-muted-foreground truncate">
753
+ {doc.stepName}
754
+ </p>
755
+ </div>
756
+ </div>
757
+ );
758
+ })}
759
+ </div>
760
+
761
+ {selectedIds.size > 0 && (
762
+ <Button
763
+ onClick={chainIntoNewWorkflow}
764
+ className="w-full gap-2"
765
+ size="sm"
766
+ >
767
+ <ArrowRight className="h-4 w-4" />
768
+ Chain {selectedIds.size} Document{selectedIds.size !== 1 ? "s" : ""} Into New Workflow
769
+ </Button>
770
+ )}
771
+ </CardContent>
772
+ </Card>
773
+ );
774
+ }
@@ -40,6 +40,7 @@ describe("npx safety: no process.cwd() for app-internal asset resolution", () =>
40
40
  const ALLOWED_FILES = [
41
41
  "bin/cli.ts", // CLI entrypoint defines cwd context
42
42
  "src/lib/environment/workspace-context.ts", // defines getLaunchCwd fallback
43
+ "src/lib/utils/app-root.ts", // validated fallback when import.meta.dirname is virtual
43
44
  "drizzle.config.ts", // build-time config
44
45
  ];
45
46
 
@@ -50,8 +51,13 @@ describe("npx safety: no process.cwd() for app-internal asset resolution", () =>
50
51
  const DANGEROUS_PATTERNS = [
51
52
  /process\.cwd\(\)\s*,\s*["'](?:public|docs|book|ai-native-notes|src)\b/,
52
53
  /process\.cwd\(\)\s*,\s*["'].*?\.(?:png|ico|svg|jpg|md|json)["']/,
54
+ // Catch bare process.cwd() in book/docs/profiles modules (even without join)
55
+ /process\.cwd\(\)/,
53
56
  ];
54
57
 
58
+ // Files in these directories are NEVER allowed to use process.cwd()
59
+ const STRICT_DIRS = ["src/lib/book/", "src/lib/docs/", "src/lib/agents/profiles/"];
60
+
55
61
  it("server-side code does not use process.cwd() for internal asset paths", () => {
56
62
  const srcFiles = collectFiles(join(PROJECT_ROOT, "src"));
57
63
  const binFiles = collectFiles(join(PROJECT_ROOT, "bin"));
@@ -69,10 +75,19 @@ describe("npx safety: no process.cwd() for app-internal asset resolution", () =>
69
75
 
70
76
  const content = readFileSync(filePath, "utf-8");
71
77
  const lines = content.split("\n");
78
+ const isStrictDir = STRICT_DIRS.some((d) => relative.startsWith(d));
72
79
 
73
80
  for (let i = 0; i < lines.length; i++) {
74
81
  const line = lines[i];
75
- for (const pattern of DANGEROUS_PATTERNS) {
82
+ // In strict dirs, ANY process.cwd() in code (not comments) is a violation
83
+ const trimmed = line.trim();
84
+ const isComment = trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*");
85
+ if (isStrictDir && !isComment && /process\.cwd\(\)/.test(line)) {
86
+ violations.push({ file: relative, line: i + 1, text: line.trim() });
87
+ continue;
88
+ }
89
+ // Elsewhere, only flag process.cwd() combined with app-internal paths
90
+ for (const pattern of DANGEROUS_PATTERNS.slice(0, 2)) {
76
91
  if (pattern.test(line)) {
77
92
  violations.push({ file: relative, line: i + 1, text: line.trim() });
78
93
  }
@@ -119,7 +134,7 @@ describe("npx safety: no process.cwd() for app-internal asset resolution", () =>
119
134
 
120
135
  it("icon assets referenced in metadata exist in public/", () => {
121
136
  const publicDir = join(PROJECT_ROOT, "public");
122
- const requiredIcons = ["stagent-s-64.png", "stagent-s-128.png", "icon.svg"];
137
+ const requiredIcons = ["stagent-s-64.png", "stagent-s-128.png"];
123
138
 
124
139
  const missing = requiredIcons.filter((name) => {
125
140
  try {
@@ -15,11 +15,14 @@ import { eq, and } from "drizzle-orm";
15
15
  * Builtins ship inside the repo at src/lib/agents/profiles/builtins/.
16
16
  * At runtime they are copied (if missing) to ~/.claude/skills/ so users
17
17
  * can customize them without touching source.
18
- * Uses import.meta.dirname (not process.cwd()) so it works under npx.
18
+ * Uses getAppRoot + known subpath because Turbopack compiles import.meta.dirname
19
+ * to a virtual /ROOT/ path that doesn't exist on the filesystem.
19
20
  */
21
+ import { getAppRoot } from "@/lib/utils/app-root";
22
+
20
23
  const BUILTINS_DIR = path.resolve(
21
- import.meta.dirname ?? __dirname,
22
- "builtins"
24
+ getAppRoot(import.meta.dirname, 4),
25
+ "src", "lib", "agents", "profiles", "builtins"
23
26
  );
24
27
 
25
28
  function getBuiltinsDir(): string {
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { existsSync, readFileSync, readdirSync } from "fs";
3
+ import { join, resolve } from "path";
4
+ import { CHAPTER_SLUGS, CHAPTER_MAPPING } from "../chapter-mapping";
5
+
6
+ const PROJECT_ROOT = resolve(__dirname, "..", "..", "..", "..");
7
+ const CHAPTERS_DIR = join(PROJECT_ROOT, "book", "chapters");
8
+
9
+ describe("book chapter slug consistency", () => {
10
+ it("CHAPTER_SLUGS is the only slug map (no duplicates in other files)", () => {
11
+ const bookDir = resolve(__dirname, "..");
12
+ const files = readdirSync(bookDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
13
+ const violations: Array<{ file: string; line: number; text: string }> = [];
14
+
15
+ for (const file of files) {
16
+ if (file === "chapter-mapping.ts") continue; // canonical source
17
+ const content = readFileSync(join(bookDir, file), "utf-8");
18
+ const lines = content.split("\n");
19
+
20
+ for (let i = 0; i < lines.length; i++) {
21
+ // Detect inline slug maps: object literals with "ch-N": "ch-N-..." patterns
22
+ if (/["']ch-\d+["']\s*:\s*["']ch-\d+-/.test(lines[i])) {
23
+ violations.push({ file, line: i + 1, text: lines[i].trim() });
24
+ }
25
+ }
26
+ }
27
+
28
+ expect(
29
+ violations,
30
+ `Found duplicate slug maps outside chapter-mapping.ts (use CHAPTER_SLUGS import instead):\n${violations
31
+ .map((v) => ` ${v.file}:${v.line} → ${v.text}`)
32
+ .join("\n")}`
33
+ ).toEqual([]);
34
+ });
35
+
36
+ it("every CHAPTER_SLUGS entry has a corresponding markdown file", () => {
37
+ const missing: string[] = [];
38
+
39
+ for (const [chapterId, slug] of Object.entries(CHAPTER_SLUGS)) {
40
+ const filePath = join(CHAPTERS_DIR, `${slug}.md`);
41
+ if (!existsSync(filePath)) {
42
+ missing.push(`${chapterId} → book/chapters/${slug}.md`);
43
+ }
44
+ }
45
+
46
+ expect(
47
+ missing,
48
+ `Chapter markdown files missing for slug entries:\n ${missing.join("\n ")}`
49
+ ).toEqual([]);
50
+ });
51
+
52
+ it("no orphan chapter files exist without a CHAPTER_SLUGS entry", () => {
53
+ const validSlugs = new Set(Object.values(CHAPTER_SLUGS));
54
+ const files = readdirSync(CHAPTERS_DIR).filter((f) => f.endsWith(".md"));
55
+ const orphans: string[] = [];
56
+
57
+ for (const file of files) {
58
+ const slug = file.replace(/\.md$/, "");
59
+ if (!validSlugs.has(slug)) {
60
+ orphans.push(file);
61
+ }
62
+ }
63
+
64
+ expect(
65
+ orphans,
66
+ `Orphan chapter files found (not in CHAPTER_SLUGS — stale or misnamed):\n ${orphans.join("\n ")}`
67
+ ).toEqual([]);
68
+ });
69
+
70
+ it("CHAPTER_SLUGS and CHAPTER_MAPPING cover the same chapter IDs", () => {
71
+ const slugIds = new Set(Object.keys(CHAPTER_SLUGS));
72
+ const mappingIds = new Set(Object.keys(CHAPTER_MAPPING));
73
+
74
+ const inSlugsOnly = [...slugIds].filter((id) => !mappingIds.has(id));
75
+ const inMappingOnly = [...mappingIds].filter((id) => !slugIds.has(id));
76
+
77
+ expect(inSlugsOnly, `In CHAPTER_SLUGS but not CHAPTER_MAPPING`).toEqual([]);
78
+ expect(inMappingOnly, `In CHAPTER_MAPPING but not CHAPTER_SLUGS`).toEqual([]);
79
+ });
80
+ });
@@ -1,7 +1,8 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
2
  import { join } from "path";
3
- import { CHAPTER_MAPPING } from "./chapter-mapping";
3
+ import { CHAPTER_MAPPING, CHAPTER_SLUGS } from "./chapter-mapping";
4
4
  import { getChapter } from "./content";
5
+ import { getAppRoot } from "../utils/app-root";
5
6
 
6
7
  /** Shared context gathered from disk for prompt assembly */
7
8
  interface ChapterContext {
@@ -29,10 +30,9 @@ export function gatherChapterContext(chapterId: string): ChapterContext {
29
30
 
30
31
  const mapping = CHAPTER_MAPPING[chapterId];
31
32
  const sourceDocSlugs = mapping?.docs ?? [];
32
- const slug = chapterIdToSlug(chapterId);
33
+ const slug = CHAPTER_SLUGS[chapterId] ?? chapterId;
33
34
 
34
- // Resolve paths relative to source file, not cwd (npx-safe)
35
- const appRoot = join(import.meta.dirname ?? __dirname, "..", "..", "..");
35
+ const appRoot = getAppRoot(import.meta.dirname, 3);
36
36
 
37
37
  // Read the current chapter markdown (if it exists)
38
38
  const chapterMdPath = join(appRoot, "book", "chapters", `${slug}.md`);
@@ -179,18 +179,3 @@ export function buildChapterRegenerationPrompt(chapterId: string): string {
179
179
  return sections.join("\n");
180
180
  }
181
181
 
182
- /** Map chapter ID to markdown filename slug */
183
- function chapterIdToSlug(chapterId: string): string {
184
- const slugMap: Record<string, string> = {
185
- "ch-1": "ch-1-project-management",
186
- "ch-2": "ch-2-task-execution",
187
- "ch-3": "ch-3-document-processing",
188
- "ch-4": "ch-4-workflow-orchestration",
189
- "ch-5": "ch-5-scheduled-intelligence",
190
- "ch-6": "ch-6-agent-self-improvement",
191
- "ch-7": "ch-7-multi-agent-swarms",
192
- "ch-8": "ch-8-human-in-the-loop",
193
- "ch-9": "ch-9-autonomous-organization",
194
- };
195
- return slugMap[chapterId] ?? chapterId;
196
- }
@@ -1,5 +1,22 @@
1
1
  /** Maps book chapters to Playbook feature docs and journeys */
2
2
 
3
+ /**
4
+ * Single source of truth: chapter ID → markdown filename slug.
5
+ * Used by content loader, chapter generator, and update detector.
6
+ * Slug corresponds to files in book/chapters/ (without .md extension).
7
+ */
8
+ export const CHAPTER_SLUGS: Record<string, string> = {
9
+ "ch-1": "ch-1-project-management",
10
+ "ch-2": "ch-2-task-execution",
11
+ "ch-3": "ch-3-document-processing",
12
+ "ch-4": "ch-4-workflow-orchestration",
13
+ "ch-5": "ch-5-scheduled-intelligence",
14
+ "ch-6": "ch-6-agent-self-improvement",
15
+ "ch-7": "ch-7-multi-agent-swarms",
16
+ "ch-8": "ch-8-human-in-the-loop",
17
+ "ch-9": "ch-9-autonomous-organization",
18
+ };
19
+
3
20
  interface ChapterMapping {
4
21
  docs: string[];
5
22
  journey?: string;
@@ -1,17 +1,5 @@
1
1
  import type { Book, BookChapter, BookPart } from "./types";
2
-
3
- /** Mapping from chapter ID to markdown filename slug (without ch-N- prefix) */
4
- const CHAPTER_SLUG_MAP: Record<string, string> = {
5
- "ch-1": "ch-1-project-management",
6
- "ch-2": "ch-2-task-execution",
7
- "ch-3": "ch-3-document-processing",
8
- "ch-4": "ch-4-workflow-orchestration",
9
- "ch-5": "ch-5-scheduled-intelligence",
10
- "ch-6": "ch-6-agent-self-improvement",
11
- "ch-7": "ch-7-multi-agent-swarms",
12
- "ch-8": "ch-8-human-in-the-loop",
13
- "ch-9": "ch-9-the-autonomous-organization",
14
- };
2
+ import { CHAPTER_SLUGS } from "./chapter-mapping";
15
3
 
16
4
  /** The three parts of the AI Native book */
17
5
  export const PARTS: BookPart[] = [
@@ -143,7 +131,7 @@ function tryLoadMarkdownChapter(id: string): BookChapter | null {
143
131
  if (typeof window !== "undefined") return null;
144
132
 
145
133
  try {
146
- const fileSlug = CHAPTER_SLUG_MAP[id];
134
+ const fileSlug = CHAPTER_SLUGS[id];
147
135
  if (!fileSlug) return null;
148
136
 
149
137
  // Dynamic require to avoid bundling fs in client builds
@@ -154,8 +142,9 @@ function tryLoadMarkdownChapter(id: string): BookChapter | null {
154
142
  // eslint-disable-next-line @typescript-eslint/no-require-imports
155
143
  const { parseMarkdownChapter } = require("./markdown-parser") as { parseMarkdownChapter: (md: string, slug: string) => { sections: Array<{ id: string; title: string; content: import("./types").ContentBlock[] }> } };
156
144
 
157
- // Resolve relative to source file, not cwd (npx-safe)
158
- const appRoot = join(import.meta.dirname ?? __dirname, "..", "..", "..");
145
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
146
+ const { getAppRoot } = require("../utils/app-root") as { getAppRoot: (metaDirname: string | undefined, depth: number) => string };
147
+ const appRoot = getAppRoot(import.meta.dirname, 3);
159
148
  const filePath = join(appRoot, "book", "chapters", `${fileSlug}.md`);
160
149
  if (!existsSync(filePath)) return null;
161
150
 
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
- import { CHAPTER_MAPPING } from "./chapter-mapping";
4
+ import { CHAPTER_MAPPING, CHAPTER_SLUGS } from "./chapter-mapping";
5
+ import { getAppRoot } from "../utils/app-root";
5
6
 
6
7
  export interface ChapterStaleness {
7
8
  chapterId: string;
@@ -15,26 +16,12 @@ export interface ChapterStaleness {
15
16
  changedFiles: string[];
16
17
  }
17
18
 
18
- /** Chapter ID to markdown filename mapping */
19
- const CHAPTER_SLUGS: Record<string, string> = {
20
- "ch-1": "ch-1-project-management",
21
- "ch-2": "ch-2-task-execution",
22
- "ch-3": "ch-3-document-processing",
23
- "ch-4": "ch-4-workflow-orchestration",
24
- "ch-5": "ch-5-scheduled-intelligence",
25
- "ch-6": "ch-6-agent-self-improvement",
26
- "ch-7": "ch-7-multi-agent-swarms",
27
- "ch-8": "ch-8-human-in-the-loop",
28
- "ch-9": "ch-9-autonomous-organization",
29
- };
30
-
31
19
  /** Read the lastGeneratedBy timestamp from a chapter's markdown frontmatter */
32
20
  function getLastGenerated(chapterId: string): string | null {
33
21
  const slug = CHAPTER_SLUGS[chapterId];
34
22
  if (!slug) return null;
35
23
 
36
- // Resolve relative to source file, not cwd (npx-safe)
37
- const appRoot = join(import.meta.dirname ?? __dirname, "..", "..", "..");
24
+ const appRoot = getAppRoot(import.meta.dirname, 3);
38
25
  const mdPath = join(appRoot, "book", "chapters", `${slug}.md`);
39
26
  if (!existsSync(mdPath)) return null;
40
27
 
@@ -299,6 +299,7 @@ export async function* sendMessage(
299
299
  "mcp__stagent__upload_document",
300
300
  "mcp__stagent__update_document",
301
301
  "mcp__stagent__delete_document",
302
+ "mcp__stagent__set_settings",
302
303
  ]);
303
304
  if (toolName.startsWith("mcp__stagent__") && !PERMISSION_GATED_TOOLS.has(toolName)) {
304
305
  // Emit tool-use status so the user sees what the model is doing
@@ -27,6 +27,7 @@ export const STAGENT_SYSTEM_PROMPT = `You are Stagent, an AI workspace assistant
27
27
  - delete_workflow: Delete a workflow and its children [requires approval]
28
28
  - execute_workflow: Start workflow execution [requires approval]
29
29
  - get_workflow_status: Get current execution status with step progress
30
+ - find_related_documents: Search the project document pool for documents to attach as workflow context
30
31
 
31
32
  ### Schedules
32
33
  - list_schedules: List all scheduled prompt loops
@@ -81,4 +82,11 @@ Be proactive with tools. If the user asks about project status, use list_tasks t
81
82
  - If a project context is active, scope operations to it unless the user specifies otherwise.
82
83
  - Tools marked [requires approval] will prompt the user before executing.
83
84
  - For workflows, valid patterns are: sequence, parallel, checkpoint, planner-executor, swarm, loop.
84
- - When a working directory is specified, always create files relative to it. Never assume the git root is the working directory — they may differ in worktree environments.`;
85
+ - When a working directory is specified, always create files relative to it. Never assume the git root is the working directory — they may differ in worktree environments.
86
+
87
+ ## Document Pool Awareness
88
+ When creating follow-up workflows that should reference documents from prior work:
89
+ 1. **Proactively discover**: Use find_related_documents to check for output documents from completed workflows in the project
90
+ 2. **Wire documents in**: Pass documentIds to create_workflow to attach pool documents as context for all steps
91
+ 3. **After execution**: Mention output documents by name and suggest they can be used in follow-up workflows
92
+ 4. **Never ask for raw IDs**: Use find_related_documents to discover documents by name or source workflow — don't make the user look up IDs manually`;
@@ -136,6 +136,7 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
136
136
 
137
137
  // ── Settings ──
138
138
  { name: "get_settings", description: "Get current Stagent settings", group: "Settings", paramHint: "key" },
139
+ { name: "set_settings", description: "Update a Stagent setting (approval required)", group: "Settings", paramHint: "key, value" },
139
140
 
140
141
  // ── Chat History ──
141
142
  { name: "list_conversations", description: "List recent chat conversations", group: "Chat", paramHint: "search, limit" },