stagent 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +226 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -1
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/projects/[id]/route.ts +37 -0
  17. package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
  18. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  19. package/src/app/api/snapshots/[id]/route.ts +44 -0
  20. package/src/app/api/snapshots/route.ts +54 -0
  21. package/src/app/api/snapshots/settings/route.ts +67 -0
  22. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  23. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  24. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  25. package/src/app/api/tables/[id]/export/route.ts +94 -0
  26. package/src/app/api/tables/[id]/history/route.ts +15 -0
  27. package/src/app/api/tables/[id]/import/route.ts +111 -0
  28. package/src/app/api/tables/[id]/route.ts +86 -0
  29. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  30. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  31. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  32. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  33. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  34. package/src/app/api/tables/route.ts +65 -0
  35. package/src/app/api/tables/templates/route.ts +92 -0
  36. package/src/app/globals.css +14 -0
  37. package/src/app/settings/page.tsx +2 -0
  38. package/src/app/tables/[id]/page.tsx +67 -0
  39. package/src/app/tables/page.tsx +21 -0
  40. package/src/app/tables/templates/page.tsx +19 -0
  41. package/src/components/book/book-reader.tsx +62 -9
  42. package/src/components/book/content-blocks.tsx +6 -1
  43. package/src/components/chat/chat-table-result.tsx +139 -0
  44. package/src/components/documents/document-browser.tsx +1 -1
  45. package/src/components/projects/project-form-sheet.tsx +3 -27
  46. package/src/components/schedules/schedule-form.tsx +5 -27
  47. package/src/components/settings/data-management-section.tsx +17 -12
  48. package/src/components/settings/database-snapshots-section.tsx +469 -0
  49. package/src/components/shared/app-sidebar.tsx +2 -0
  50. package/src/components/shared/document-picker-sheet.tsx +214 -11
  51. package/src/components/tables/table-browser.tsx +234 -0
  52. package/src/components/tables/table-cell-editor.tsx +226 -0
  53. package/src/components/tables/table-chart-builder.tsx +288 -0
  54. package/src/components/tables/table-chart-view.tsx +146 -0
  55. package/src/components/tables/table-column-header.tsx +103 -0
  56. package/src/components/tables/table-column-sheet.tsx +331 -0
  57. package/src/components/tables/table-create-sheet.tsx +240 -0
  58. package/src/components/tables/table-detail-sheet.tsx +144 -0
  59. package/src/components/tables/table-detail-tabs.tsx +278 -0
  60. package/src/components/tables/table-grid.tsx +61 -0
  61. package/src/components/tables/table-history-tab.tsx +148 -0
  62. package/src/components/tables/table-import-wizard.tsx +542 -0
  63. package/src/components/tables/table-list-table.tsx +95 -0
  64. package/src/components/tables/table-relation-combobox.tsx +217 -0
  65. package/src/components/tables/table-row-sheet.tsx +271 -0
  66. package/src/components/tables/table-spreadsheet.tsx +394 -0
  67. package/src/components/tables/table-template-gallery.tsx +162 -0
  68. package/src/components/tables/table-template-preview.tsx +219 -0
  69. package/src/components/tables/table-toolbar.tsx +79 -0
  70. package/src/components/tables/table-triggers-tab.tsx +446 -0
  71. package/src/components/tables/types.ts +6 -0
  72. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  73. package/src/components/tables/utils.ts +29 -0
  74. package/src/components/tasks/task-create-panel.tsx +5 -31
  75. package/src/components/tasks/task-edit-dialog.tsx +5 -27
  76. package/src/components/workflows/workflow-form-view.tsx +11 -35
  77. package/src/components/workflows/workflow-status-view.tsx +1 -1
  78. package/src/instrumentation.ts +3 -0
  79. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  80. package/src/lib/agents/claude-agent.ts +3 -1
  81. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +23 -0
  82. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
  83. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
  84. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  85. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  86. package/src/lib/book/chapter-generator.ts +81 -5
  87. package/src/lib/book/chapter-mapping.ts +58 -24
  88. package/src/lib/book/content.ts +83 -47
  89. package/src/lib/book/markdown-parser.ts +1 -1
  90. package/src/lib/book/reading-paths.ts +8 -8
  91. package/src/lib/book/types.ts +1 -1
  92. package/src/lib/book/update-detector.ts +4 -1
  93. package/src/lib/chat/stagent-tools.ts +2 -0
  94. package/src/lib/chat/tool-catalog.ts +34 -0
  95. package/src/lib/chat/tools/table-tools.ts +955 -0
  96. package/src/lib/chat/tools/workflow-tools.ts +9 -1
  97. package/src/lib/constants/table-status.ts +68 -0
  98. package/src/lib/data/__tests__/clear.test.ts +1 -1
  99. package/src/lib/data/clear.ts +45 -0
  100. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  101. package/src/lib/data/seed-data/conversations.ts +350 -42
  102. package/src/lib/data/seed-data/documents.ts +564 -591
  103. package/src/lib/data/seed-data/learned-context.ts +101 -22
  104. package/src/lib/data/seed-data/notifications.ts +344 -70
  105. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  106. package/src/lib/data/seed-data/profiles.ts +144 -46
  107. package/src/lib/data/seed-data/projects.ts +50 -18
  108. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  109. package/src/lib/data/seed-data/schedules.ts +208 -41
  110. package/src/lib/data/seed-data/table-templates.ts +234 -0
  111. package/src/lib/data/seed-data/tasks.ts +614 -116
  112. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  113. package/src/lib/data/seed-data/user-tables.ts +203 -0
  114. package/src/lib/data/seed-data/views.ts +52 -7
  115. package/src/lib/data/seed-data/workflows.ts +231 -84
  116. package/src/lib/data/seed.ts +55 -14
  117. package/src/lib/data/tables.ts +417 -0
  118. package/src/lib/db/bootstrap.ts +227 -0
  119. package/src/lib/db/index.ts +9 -0
  120. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  121. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  122. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  123. package/src/lib/db/schema.ts +368 -0
  124. package/src/lib/snapshots/auto-backup.ts +132 -0
  125. package/src/lib/snapshots/retention.ts +64 -0
  126. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  127. package/src/lib/tables/computed.ts +61 -0
  128. package/src/lib/tables/context-builder.ts +139 -0
  129. package/src/lib/tables/formula-engine.ts +415 -0
  130. package/src/lib/tables/history.ts +115 -0
  131. package/src/lib/tables/import.ts +343 -0
  132. package/src/lib/tables/query-builder.ts +152 -0
  133. package/src/lib/tables/trigger-evaluator.ts +146 -0
  134. package/src/lib/tables/types.ts +141 -0
  135. package/src/lib/tables/validation.ts +119 -0
  136. package/src/lib/utils/stagent-paths.ts +20 -0
  137. package/src/lib/workflows/types.ts +1 -1
  138. package/tsconfig.json +3 -1
  139. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -172,39 +172,11 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
172
172
  : null;
173
173
 
174
174
  const handleDocPickerConfirm = useCallback(
175
- (ids: string[]) => {
175
+ (ids: string[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
176
176
  setSelectedDocIds(new Set(ids));
177
- // Fetch metadata for newly selected docs
178
- const newIds = ids.filter(
179
- (id) => !selectedDocs.some((d) => d.id === id)
180
- );
181
- if (newIds.length > 0) {
182
- const params = new URLSearchParams({ status: "ready" });
183
- if (projectId) params.set("projectId", projectId);
184
- fetch(`/api/documents?${params}`)
185
- .then((r) => r.json())
186
- .then((allDocs: Array<Record<string, unknown>>) => {
187
- const idSet = new Set(ids);
188
- setSelectedDocs(
189
- allDocs
190
- .filter((d) => idSet.has(d.id as string))
191
- .map((d) => ({
192
- id: d.id as string,
193
- originalName: d.originalName as string,
194
- mimeType: d.mimeType as string,
195
- size: d.size as number,
196
- }))
197
- );
198
- })
199
- .catch(() => {});
200
- } else {
201
- // Remove deselected docs
202
- setSelectedDocs((prev) =>
203
- prev.filter((d) => ids.includes(d.id))
204
- );
205
- }
177
+ setSelectedDocs(meta);
206
178
  },
207
- [projectId, selectedDocs]
179
+ []
208
180
  );
209
181
 
210
182
  async function handleSubmit(e: React.FormEvent) {
@@ -497,6 +469,8 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
497
469
  onConfirm={handleDocPickerConfirm}
498
470
  groupBy="project"
499
471
  title="Select Context Documents"
472
+ allowCrossProject
473
+ selectedDocumentMeta={selectedDocs}
500
474
  />
501
475
  </FormSectionCard>
502
476
 
@@ -124,35 +124,11 @@ export function TaskEditDialog({
124
124
  : null;
125
125
 
126
126
  const handleDocPickerConfirm = useCallback(
127
- (ids: string[]) => {
127
+ (ids: string[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
128
128
  setSelectedDocIds(new Set(ids));
129
- const newIds = ids.filter(
130
- (id) => !selectedDocs.some((d) => d.id === id)
131
- );
132
- if (newIds.length > 0) {
133
- const params = new URLSearchParams({ status: "ready" });
134
- if (task?.projectId) params.set("projectId", task.projectId);
135
- fetch(`/api/documents?${params}`)
136
- .then((r) => r.json())
137
- .then((allDocs: Array<Record<string, unknown>>) => {
138
- const idSet = new Set(ids);
139
- setSelectedDocs(
140
- allDocs
141
- .filter((d) => idSet.has(d.id as string))
142
- .map((d) => ({
143
- id: d.id as string,
144
- originalName: d.originalName as string,
145
- mimeType: d.mimeType as string,
146
- size: d.size as number,
147
- }))
148
- );
149
- })
150
- .catch(() => {});
151
- } else {
152
- setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
153
- }
129
+ setSelectedDocs(meta);
154
130
  },
155
- [task?.projectId, selectedDocs]
131
+ []
156
132
  );
157
133
 
158
134
  async function handleSubmit(e: React.FormEvent) {
@@ -384,6 +360,8 @@ export function TaskEditDialog({
384
360
  onConfirm={handleDocPickerConfirm}
385
361
  groupBy="project"
386
362
  title="Select Context Documents"
363
+ allowCrossProject
364
+ selectedDocumentMeta={selectedDocs}
387
365
  />
388
366
 
389
367
  <Button
@@ -155,14 +155,14 @@ function normalizeParallelSteps(
155
155
 
156
156
  const normalizedBranches = branches.map((branch, index) => ({
157
157
  ...branch,
158
- id: options?.cloneIds ? crypto.randomUUID() : branch.id,
158
+ id: options?.cloneIds ? crypto.randomUUID() : (branch.id || crypto.randomUUID()),
159
159
  name: branch.name || `Research Branch ${index + 1}`,
160
160
  }));
161
161
 
162
162
  const normalizedSynthesis = rawSynthesis
163
163
  ? {
164
164
  ...rawSynthesis,
165
- id: options?.cloneIds ? crypto.randomUUID() : rawSynthesis.id,
165
+ id: options?.cloneIds ? crypto.randomUUID() : (rawSynthesis.id || crypto.randomUUID()),
166
166
  name: rawSynthesis.name || "Synthesize findings",
167
167
  }
168
168
  : undefined;
@@ -257,7 +257,7 @@ function normalizeSwarmSteps(
257
257
  id:
258
258
  options?.cloneIds && mayorStep
259
259
  ? crypto.randomUUID()
260
- : (mayorStep?.id ?? crypto.randomUUID()),
260
+ : (mayorStep?.id || crypto.randomUUID()),
261
261
  name: mayorStep?.name || "Mayor plan",
262
262
  };
263
263
 
@@ -268,7 +268,7 @@ function normalizeSwarmSteps(
268
268
 
269
269
  const normalizedWorkers = nextWorkers.map((worker, index) => ({
270
270
  ...worker,
271
- id: options?.cloneIds ? crypto.randomUUID() : worker.id,
271
+ id: options?.cloneIds ? crypto.randomUUID() : (worker.id || crypto.randomUUID()),
272
272
  name: worker.name || `Worker ${index + 1}`,
273
273
  }));
274
274
 
@@ -277,7 +277,7 @@ function normalizeSwarmSteps(
277
277
  id:
278
278
  options?.cloneIds && refineryStep
279
279
  ? crypto.randomUUID()
280
- : (refineryStep?.id ?? crypto.randomUUID()),
280
+ : (refineryStep?.id || crypto.randomUUID()),
281
281
  name: refineryStep?.name || "Refine and merge",
282
282
  };
283
283
 
@@ -379,37 +379,11 @@ export function WorkflowFormView({
379
379
 
380
380
  // Handle document picker confirmation
381
381
  const handleDocPickerConfirm = useCallback(
382
- (ids: string[]) => {
382
+ (ids: string[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
383
383
  setSelectedDocIds(new Set(ids));
384
- // Fetch metadata for newly selected docs
385
- const newIds = ids.filter(
386
- (id) => !selectedDocs.some((d) => d.id === id)
387
- );
388
- if (newIds.length > 0) {
389
- fetch(`/api/documents?projectId=${projectId}&status=ready`)
390
- .then((r) => r.json())
391
- .then((allDocs: Array<Record<string, unknown>>) => {
392
- const idSet = new Set(ids);
393
- setSelectedDocs(
394
- allDocs
395
- .filter((d) => idSet.has(d.id as string))
396
- .map((d) => ({
397
- id: d.id as string,
398
- originalName: d.originalName as string,
399
- mimeType: d.mimeType as string,
400
- size: d.size as number,
401
- }))
402
- );
403
- })
404
- .catch(() => {});
405
- } else {
406
- // Remove deselected docs
407
- setSelectedDocs((prev) =>
408
- prev.filter((d) => ids.includes(d.id))
409
- );
410
- }
384
+ setSelectedDocs(meta);
411
385
  },
412
- [projectId, selectedDocs]
386
+ []
413
387
  );
414
388
 
415
389
  function removeDocument(id: string) {
@@ -498,7 +472,7 @@ export function WorkflowFormView({
498
472
  ? normalizeParallelSteps(def.steps)
499
473
  : def.pattern === "swarm"
500
474
  ? normalizeSwarmSteps(def.steps)
501
- : def.steps
475
+ : def.steps.map((s) => ({ ...s, id: s.id || crypto.randomUUID() }))
502
476
  );
503
477
  }
504
478
  }
@@ -1261,6 +1235,8 @@ export function WorkflowFormView({
1261
1235
  selectedIds={selectedDocIds}
1262
1236
  onConfirm={handleDocPickerConfirm}
1263
1237
  groupBy="workflow"
1238
+ allowCrossProject
1239
+ selectedDocumentMeta={selectedDocs}
1264
1240
  />
1265
1241
  </FormSectionCard>
1266
1242
  )}
@@ -544,7 +544,7 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
544
544
  ) : (
545
545
  <div className="space-y-3">
546
546
  {data.steps.map((step, index) => (
547
- <div key={step.id} className="flex items-start gap-3">
547
+ <div key={`${step.id}-${index}`} className="flex items-start gap-3">
548
548
  <div className="mt-0.5 flex flex-col items-center">
549
549
  {stepStatusIcons[step.state.status] ?? stepStatusIcons.pending}
550
550
  {index < data.steps.length - 1 && (
@@ -6,5 +6,8 @@ export async function register() {
6
6
 
7
7
  const { startChannelPoller } = await import("@/lib/channels/poller");
8
8
  startChannelPoller();
9
+
10
+ const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
11
+ startAutoBackup();
9
12
  }
10
13
  }
@@ -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
 
@@ -14,3 +14,26 @@ You are a technical writer producing clear, well-structured documents.
14
14
  - Include a table of contents for documents with 3+ sections
15
15
  - Highlight action items or decisions needed in bold
16
16
  - If writing from a template, preserve the template's style and structure
17
+
18
+ ## Book Chapter Conventions
19
+
20
+ When generating AI Native book chapters:
21
+
22
+ - Preserve existing `> [!authors-note]` blocks unchanged during regeneration
23
+ - Preserve existing `> [!case-study]` blocks — update content only if source material changed
24
+ - Include "Building with Stagent" TypeScript code examples with realistic values
25
+ - Include both "Stagent Today" and "Roadmap Vision" sections
26
+ - Use `> [!case-study]` callout format: name the company, describe their pattern, draw parallel to Stagent
27
+ - Follow the Problem → Solution → Implementation → Lessons narrative arc
28
+ - Target the reading time specified in chapter frontmatter (~250 words/min)
29
+
30
+ ## Originality and Attribution Rules
31
+
32
+ When writing chapters that reference external case studies (ai-native-notes/ articles):
33
+
34
+ - **Never copy phrases verbatim** from source articles without quotation marks and explicit attribution
35
+ - **Always credit authors by name** in case-study callouts (e.g., "Geoffrey Huntley" not just "Ralph Wiggum", "Dorsey and Botha" not just "Sequoia")
36
+ - **When structuring content around an external framework** (e.g., 8090's five stations, Block's four pillars), explicitly acknowledge the source: "As [Author] describes in [Work]..." before elaborating
37
+ - **Synthesize from multiple sources** rather than mirroring a single article's structure. If one source dominates a section, bring in at least one additional perspective
38
+ - **Make it Stagent's own**: Every external concept should connect to Stagent's concrete implementation or roadmap. Don't just restate what Stripe/Ramp/Harvey built — explain what Stagent builds differently and why
39
+ - **Use direct quotes sparingly** and only for memorable, well-attributed phrases. The majority of prose should be original analysis
@@ -29,3 +29,13 @@ Structure documentation according to the type requested:
29
29
  - **ADR**: Title, status (proposed/accepted/deprecated/superseded), context, decision, consequences
30
30
  - **README**: Title, description, prerequisites, installation, usage, configuration, contributing, license
31
31
  - **Changelog**: Version header, date, categorized entries with PR/issue references where available
32
+ - **Book Chapter Review**: Quality pass on AI Native book chapters — terminology consistency, API accuracy, case study attribution, code example quality, section presence, grammar/style. Edit in-place; do not rewrite.
33
+
34
+ ## Originality Checks (Book Chapter Review)
35
+
36
+ During book chapter quality reviews, specifically check for:
37
+
38
+ - **Verbatim copying**: Flag any passage >15 words matching source articles in `ai-native-notes/` that lacks quotation marks
39
+ - **Author attribution**: Every case-study callout must name the author (e.g., "Geoffrey Huntley" not "Ralph Wiggum", "Dorsey and Botha" not "Sequoia")
40
+ - **Structural mirroring**: If a chapter follows a source article's exact progression without acknowledgment, flag it and suggest an explicit framing line ("As [Author] traces in [Work]...")
41
+ - **Single-source dependency**: If >50% of a section's ideas come from one source, flag it and suggest multi-source synthesis
@@ -2,7 +2,7 @@ id: technical-writer
2
2
  name: Technical Writer
3
3
  version: "1.0.0"
4
4
  domain: work
5
- tags: [documentation, api-docs, adr, readme, changelog, technical-writing]
5
+ tags: [documentation, api-docs, adr, readme, changelog, technical-writing, book-chapter-review]
6
6
  supportedRuntimes: [claude-code, openai-codex-app-server, anthropic-direct, openai-direct, ollama]
7
7
  preferredRuntime: anthropic-direct
8
8
  allowedTools:
@@ -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,
@@ -18,6 +18,7 @@ interface ChapterContext {
18
18
  relatedJourney: string | undefined;
19
19
  currentMarkdown: string | null;
20
20
  sourceContents: string[];
21
+ caseStudyContents: string[];
21
22
  strategy: string | null;
22
23
  }
23
24
 
@@ -50,6 +51,17 @@ export function gatherChapterContext(chapterId: string): ChapterContext {
50
51
  }
51
52
  }
52
53
 
54
+ // Read case study articles from ai-native-notes/
55
+ const caseStudySlugs = mapping?.caseStudies ?? [];
56
+ const caseStudyContents: string[] = [];
57
+ for (const csSlug of caseStudySlugs) {
58
+ const csPath = join(appRoot, "ai-native-notes", `${csSlug}.md`);
59
+ if (existsSync(csPath)) {
60
+ const content = readFileSync(csPath, "utf-8");
61
+ caseStudyContents.push(`### Case Study: ${csSlug}\n${content}`);
62
+ }
63
+ }
64
+
53
65
  // Read the book strategy document
54
66
  const strategyPath = join(appRoot, "ai-native-notes", "ai-native-book-strategy.md");
55
67
  const strategy = existsSync(strategyPath)
@@ -69,6 +81,7 @@ export function gatherChapterContext(chapterId: string): ChapterContext {
69
81
  relatedJourney: chapter.relatedJourney,
70
82
  currentMarkdown,
71
83
  sourceContents,
84
+ caseStudyContents,
72
85
  strategy,
73
86
  };
74
87
  }
@@ -96,10 +109,14 @@ export function buildChapterRegenerationPrompt(chapterId: string): string {
96
109
  `${isNew ? "Write" : "Regenerate"} this book chapter following these rules:`,
97
110
  "1. **Narrative voice**: Write in first-person plural ('we') with a technical-but-approachable tone",
98
111
  "2. **Structure**: Follow the Problem → Solution → Implementation → Lessons pattern",
99
- "3. **Code examples**: Include real code snippets from the Stagent codebase with filename comments",
100
- "4. **Markdown format**: Use the conventions below for content blocks",
101
- "5. **Reading time**: Target approximately " + ctx.readingTime + " minutes",
102
- ...(isNew ? [] : ["6. **Preserve Author's Notes**: Keep any existing `> [!authors-note]` blocks unchanged"]),
112
+ "3. **Code examples**: Include 'Building with Stagent' TypeScript API examples showing how developers use the feature (realistic UUIDs, timestamps, async/await, comments on non-obvious lines)",
113
+ "4. **Case studies**: Weave 2-4 case study references as `> [!case-study]` callout blocks. Name the company, describe their pattern, draw a parallel to Stagent.",
114
+ "5. **Stagent sections**: Each chapter must include a 'Stagent Today' section (current implementation) and a 'Roadmap Vision' section (future direction informed by case studies).",
115
+ "6. **Narrative thread**: Reference the 'machine that builds machines' thesis how Stagent uses its own capabilities to build/maintain itself.",
116
+ "7. **Originality**: Never copy phrases verbatim from case study material without quotation marks. Always credit authors by name in callouts. Synthesize from multiple sources rather than mirroring one article's structure. Make content Stagent's own — connect every external concept to Stagent's implementation.",
117
+ "8. **Markdown format**: Use the conventions below for content blocks",
118
+ "9. **Reading time**: Target approximately " + ctx.readingTime + " minutes",
119
+ ...(isNew ? [] : ["10. **Preserve Author's Notes**: Keep any existing `> [!authors-note]` blocks unchanged"]),
103
120
  "",
104
121
  "## Tools Available",
105
122
  "",
@@ -111,7 +128,7 @@ export function buildChapterRegenerationPrompt(chapterId: string): string {
111
128
  "",
112
129
  "- Sections: `## Section Title`",
113
130
  "- Code blocks: ` ```language ` with `<!-- filename: path -->` before the block",
114
- "- Callouts: `> [!tip]`, `> [!warning]`, `> [!info]`, `> [!lesson]`, `> [!authors-note]`",
131
+ "- Callouts: `> [!tip]`, `> [!warning]`, `> [!info]`, `> [!lesson]`, `> [!authors-note]`, `> [!case-study]`",
115
132
  "- Interactive links: `[Try: label](href)`",
116
133
  "- Images: `![alt](src \"caption\")`",
117
134
  "",
@@ -141,6 +158,19 @@ export function buildChapterRegenerationPrompt(chapterId: string): string {
141
158
  );
142
159
  }
143
160
 
161
+ if (ctx.caseStudyContents.length > 0) {
162
+ sections.push(
163
+ "## Case Study Material",
164
+ "",
165
+ "Weave these case studies into the narrative as `> [!case-study]` callout blocks.",
166
+ "Each callout should: name the company, describe their specific pattern, and draw a parallel to the Stagent feature being discussed.",
167
+ "Do NOT create a separate 'Case Studies' section — integrate them inline where relevant.",
168
+ "",
169
+ ...ctx.caseStudyContents.map(c => c.slice(0, 3000)), // Truncate each to manage context
170
+ ""
171
+ );
172
+ }
173
+
144
174
  if (ctx.strategy) {
145
175
  sections.push(
146
176
  "## Book Strategy Reference",
@@ -179,3 +209,49 @@ export function buildChapterRegenerationPrompt(chapterId: string): string {
179
209
  return sections.join("\n");
180
210
  }
181
211
 
212
+ /**
213
+ * Build an agent prompt for the technical-writer profile to quality-review
214
+ * a generated book chapter. This is a post-generation pass for consistency,
215
+ * accuracy, and style.
216
+ */
217
+ export function buildChapterQualityPrompt(chapterId: string): string {
218
+ const ctx = gatherChapterContext(chapterId);
219
+ if (!ctx.currentMarkdown) {
220
+ throw new Error(`No chapter content found for ${chapterId} — generate it first`);
221
+ }
222
+
223
+ const sections: string[] = [
224
+ `# Chapter Quality Review: ${ctx.title}`,
225
+ "",
226
+ `Review Chapter ${ctx.chapterNumber}: "${ctx.title}" — ${ctx.subtitle}`,
227
+ "",
228
+ "## Review Checklist",
229
+ "",
230
+ "1. **Terminology consistency** — verify consistent use of terms across the chapter (e.g., 'agent profile' not 'agent persona', 'workflow' not 'pipeline' when referring to Stagent workflows)",
231
+ "2. **API accuracy** — read referenced source files to confirm code examples use correct API paths, parameter names, and response shapes",
232
+ "3. **Case study attribution** — every `> [!case-study]` callout must name the company, author (if known), and approximate date",
233
+ "4. **Code example quality** — 'Building with Stagent' examples should use realistic values (UUIDs, timestamps), async/await, and brief comments on non-obvious lines",
234
+ "5. **Section presence** — verify the chapter contains 'Stagent Today' and 'Roadmap Vision' sections",
235
+ "6. **Grammar and style** — first-person plural voice ('we'), technical but approachable, no marketing fluff",
236
+ "7. **Reading time** — content should match the target of ~" + ctx.readingTime + " minutes (~250 words/min)",
237
+ "",
238
+ "## Tools Available",
239
+ "",
240
+ "You have access to **Read**, **Grep**, **Glob**, **Write**, and **Edit** tools.",
241
+ "- Use **Read** and **Grep** to verify API paths and source code references",
242
+ "- Use **Edit** to make corrections directly in the chapter file",
243
+ "",
244
+ "## Chapter Content",
245
+ "",
246
+ "```markdown",
247
+ ctx.currentMarkdown,
248
+ "```",
249
+ "",
250
+ "## Output",
251
+ "",
252
+ `Edit the chapter file at: \`book/chapters/${ctx.slug}.md\``,
253
+ "Make corrections in-place. Do not rewrite the entire chapter — only fix issues found in the review checklist.",
254
+ ];
255
+
256
+ return sections.join("\n");
257
+ }