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.
- package/dist/cli.js +47 -1
- package/package.json +1 -2
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +35 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +133 -2
- package/src/components/schedules/schedule-form.tsx +113 -1
- package/src/components/shared/document-picker-sheet.tsx +283 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +137 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +125 -2
- package/src/components/workflows/workflow-form-view.tsx +231 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +167 -22
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +1 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/data/clear.ts +12 -0
- package/src/lib/db/bootstrap.ts +48 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/schema.ts +77 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/public/icon.svg +0 -13
- 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
|
|
334
|
-
{
|
|
360
|
+
{/* Edit — draft, completed, or failed */}
|
|
361
|
+
{["draft", "completed", "failed"].includes(data.status) && hasDefinition && (
|
|
335
362
|
<Button
|
|
336
|
-
variant="
|
|
337
|
-
size="
|
|
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="
|
|
350
|
-
size="
|
|
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="
|
|
363
|
-
size="
|
|
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="
|
|
377
|
-
size="
|
|
378
|
-
className="
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
|
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 =
|
|
33
|
+
const slug = CHAPTER_SLUGS[chapterId] ?? chapterId;
|
|
33
34
|
|
|
34
|
-
|
|
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;
|
package/src/lib/book/content.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
158
|
-
const
|
|
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
|
-
|
|
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
|
|
package/src/lib/chat/engine.ts
CHANGED
|
@@ -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" },
|