stagent 0.6.2 → 0.7.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.
- package/README.md +21 -2
- package/dist/cli.js +272 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -2
- package/src/app/api/chat/entities/search/route.ts +12 -3
- 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 +72 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- 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/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- 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 +109 -2
- package/src/components/schedules/schedule-form.tsx +91 -1
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +486 -0
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +111 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +103 -2
- package/src/components/workflows/workflow-form-view.tsx +207 -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 +168 -23
- package/src/instrumentation.ts +3 -0
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- 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/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +35 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +57 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +275 -0
- package/src/lib/db/index.ts +9 -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/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +445 -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/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/utils/stagent-paths.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/tsconfig.json +3 -1
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -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>
|
|
@@ -520,7 +544,7 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
|
|
|
520
544
|
) : (
|
|
521
545
|
<div className="space-y-3">
|
|
522
546
|
{data.steps.map((step, index) => (
|
|
523
|
-
<div key={step.id} className="flex items-start gap-3">
|
|
547
|
+
<div key={`${step.id}-${index}`} className="flex items-start gap-3">
|
|
524
548
|
<div className="mt-0.5 flex flex-col items-center">
|
|
525
549
|
{stepStatusIcons[step.state.status] ?? stepStatusIcons.pending}
|
|
526
550
|
{index < data.steps.length - 1 && (
|
|
@@ -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
|
+
}
|
package/src/instrumentation.ts
CHANGED
|
@@ -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 {
|
|
@@ -32,6 +32,8 @@ const {
|
|
|
32
32
|
update: vi.fn().mockReturnValue({ set: mockSet }),
|
|
33
33
|
insert: vi.fn().mockReturnValue({ values: mockValues }),
|
|
34
34
|
};
|
|
35
|
+
// .where() must return a thenable with .all() for both async and sync query patterns
|
|
36
|
+
mockWhere.mockReturnValue({ then: (fn: (v: unknown[]) => void) => fn([]), all: () => [] });
|
|
35
37
|
mockFrom.mockReturnValue({ where: mockWhere });
|
|
36
38
|
mockSet.mockReturnValue({ where: mockSetWhere });
|
|
37
39
|
mockValues.mockResolvedValue(undefined);
|
|
@@ -77,7 +79,8 @@ const {
|
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
vi.mock("@/lib/db", () => ({ db: mockDb }));
|
|
80
|
-
vi.mock("@/lib/db/schema", () => ({
|
|
82
|
+
vi.mock("@/lib/db/schema", async (importOriginal) => ({
|
|
83
|
+
...(await importOriginal<typeof import("@/lib/db/schema")>()),
|
|
81
84
|
tasks: {
|
|
82
85
|
id: "id",
|
|
83
86
|
status: "status",
|
|
@@ -181,6 +184,7 @@ beforeEach(() => {
|
|
|
181
184
|
vi.resetAllMocks();
|
|
182
185
|
// Re-establish mock chains after clearAllMocks
|
|
183
186
|
mockDb.select.mockReturnValue({ from: mockFrom });
|
|
187
|
+
mockWhere.mockReturnValue({ then: (fn: (v: unknown[]) => void) => fn([]), all: () => [] });
|
|
184
188
|
mockFrom.mockReturnValue({ where: mockWhere });
|
|
185
189
|
mockDb.update.mockReturnValue({ set: mockSet });
|
|
186
190
|
mockSet.mockReturnValue({ where: mockSetWhere });
|
|
@@ -6,6 +6,7 @@ import { setExecution, removeExecution } from "./execution-manager";
|
|
|
6
6
|
import { MAX_RESUME_COUNT, DEFAULT_MAX_TURNS, DEFAULT_MAX_BUDGET_USD } from "@/lib/constants/task-status";
|
|
7
7
|
import { getAuthEnv, updateAuthStatus } from "@/lib/settings/auth";
|
|
8
8
|
import { buildDocumentContext } from "@/lib/documents/context-builder";
|
|
9
|
+
import { buildTableContext } from "@/lib/tables/context-builder";
|
|
9
10
|
import {
|
|
10
11
|
buildTaskOutputInstructions,
|
|
11
12
|
prepareTaskOutputDirectory,
|
|
@@ -369,6 +370,7 @@ export async function buildTaskQueryContext(
|
|
|
369
370
|
const profileInstructions = payload?.instructions ?? "";
|
|
370
371
|
const basePrompt = task.description || task.title;
|
|
371
372
|
const docContext = await buildDocumentContext(task.id);
|
|
373
|
+
const tableContext = await buildTableContext(task.id);
|
|
372
374
|
const outputInstructions = buildTaskOutputInstructions(task.id);
|
|
373
375
|
const learnedCtx = getActiveLearnedContext(profileId);
|
|
374
376
|
const learnedCtxBlock = learnedCtx
|
|
@@ -394,7 +396,7 @@ export async function buildTaskQueryContext(
|
|
|
394
396
|
: "";
|
|
395
397
|
|
|
396
398
|
// F1: Separate system instructions from user content
|
|
397
|
-
const systemInstructions = [worktreeNote, profileInstructions, learnedCtxBlock, docContext, outputInstructions]
|
|
399
|
+
const systemInstructions = [worktreeNote, profileInstructions, learnedCtxBlock, docContext, tableContext, outputInstructions]
|
|
398
400
|
.filter(Boolean)
|
|
399
401
|
.join("\n\n");
|
|
400
402
|
|
|
@@ -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 {
|
|
@@ -39,6 +39,11 @@ import {
|
|
|
39
39
|
recordUsageLedgerEntry,
|
|
40
40
|
resolveUsageActivityType,
|
|
41
41
|
} from "@/lib/usage/ledger";
|
|
42
|
+
import {
|
|
43
|
+
scanTaskOutputDocuments,
|
|
44
|
+
prepareTaskOutputDirectory,
|
|
45
|
+
buildTaskOutputInstructions,
|
|
46
|
+
} from "@/lib/documents/output-scanner";
|
|
42
47
|
|
|
43
48
|
// ── SDK lazy import ──────────────────────────────────────────────────
|
|
44
49
|
|
|
@@ -283,6 +288,14 @@ async function executeAnthropicDirectTask(taskId: string, isResume = false): Pro
|
|
|
283
288
|
.where(eq(tasks.id, taskId));
|
|
284
289
|
|
|
285
290
|
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
291
|
+
|
|
292
|
+
// Prepare output directory so the agent can write output files
|
|
293
|
+
if (!isResume) {
|
|
294
|
+
await prepareTaskOutputDirectory(taskId);
|
|
295
|
+
}
|
|
296
|
+
const outputInstructions = buildTaskOutputInstructions(taskId);
|
|
297
|
+
ctx.systemInstructions = `${ctx.systemInstructions}\n\n${outputInstructions}`;
|
|
298
|
+
|
|
286
299
|
const apiKey = await getAnthropicApiKey();
|
|
287
300
|
const sdk = await getAnthropicSDK();
|
|
288
301
|
const client = new sdk.default({ apiKey });
|
|
@@ -431,6 +444,22 @@ async function executeAnthropicDirectTask(taskId: string, isResume = false): Pro
|
|
|
431
444
|
timestamp: new Date(),
|
|
432
445
|
});
|
|
433
446
|
|
|
447
|
+
// Scan output directory for generated documents
|
|
448
|
+
try {
|
|
449
|
+
await scanTaskOutputDocuments(taskId);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
await db.insert(agentLogs).values({
|
|
452
|
+
id: crypto.randomUUID(),
|
|
453
|
+
taskId,
|
|
454
|
+
agentType: agentProfileId,
|
|
455
|
+
event: "output_scan_failed",
|
|
456
|
+
payload: JSON.stringify({
|
|
457
|
+
error: error instanceof Error ? error.message : String(error),
|
|
458
|
+
}),
|
|
459
|
+
timestamp: new Date(),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
434
463
|
// Record usage
|
|
435
464
|
await recordUsageLedgerEntry({
|
|
436
465
|
taskId,
|
|
@@ -39,6 +39,11 @@ import {
|
|
|
39
39
|
recordUsageLedgerEntry,
|
|
40
40
|
resolveUsageActivityType,
|
|
41
41
|
} from "@/lib/usage/ledger";
|
|
42
|
+
import {
|
|
43
|
+
scanTaskOutputDocuments,
|
|
44
|
+
prepareTaskOutputDirectory,
|
|
45
|
+
buildTaskOutputInstructions,
|
|
46
|
+
} from "@/lib/documents/output-scanner";
|
|
42
47
|
|
|
43
48
|
// ── SDK lazy import ──────────────────────────────────────────────────
|
|
44
49
|
|
|
@@ -209,6 +214,14 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
|
|
|
209
214
|
.where(eq(tasks.id, taskId));
|
|
210
215
|
|
|
211
216
|
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
217
|
+
|
|
218
|
+
// Prepare output directory so the agent can write output files
|
|
219
|
+
if (!isResume) {
|
|
220
|
+
await prepareTaskOutputDirectory(taskId);
|
|
221
|
+
}
|
|
222
|
+
const outputInstructions = buildTaskOutputInstructions(taskId);
|
|
223
|
+
ctx.systemInstructions = `${ctx.systemInstructions}\n\n${outputInstructions}`;
|
|
224
|
+
|
|
212
225
|
const apiKey = await getOpenAIApiKeyValue();
|
|
213
226
|
const sdk = await getOpenAISDK();
|
|
214
227
|
const client = new sdk.default({ apiKey });
|
|
@@ -347,6 +360,22 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
|
|
|
347
360
|
timestamp: new Date(),
|
|
348
361
|
});
|
|
349
362
|
|
|
363
|
+
// Scan output directory for generated documents
|
|
364
|
+
try {
|
|
365
|
+
await scanTaskOutputDocuments(taskId);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
await db.insert(agentLogs).values({
|
|
368
|
+
id: crypto.randomUUID(),
|
|
369
|
+
taskId,
|
|
370
|
+
agentType: agentProfileId,
|
|
371
|
+
event: "output_scan_failed",
|
|
372
|
+
payload: JSON.stringify({
|
|
373
|
+
error: error instanceof Error ? error.message : String(error),
|
|
374
|
+
}),
|
|
375
|
+
timestamp: new Date(),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
350
379
|
await recordUsageLedgerEntry({
|
|
351
380
|
taskId,
|
|
352
381
|
workflowId: task.workflowId ?? null,
|
|
@@ -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;
|