stagent 0.7.0 → 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.
@@ -14,17 +14,14 @@ import { Button } from "@/components/ui/button";
14
14
  import { Plus } from "lucide-react";
15
15
  import { toast } from "sonner";
16
16
  import { cn } from "@/lib/utils";
17
- import { CellEditor, CellDisplay } from "./table-cell-editor";
17
+ import { CellDisplay } from "./table-cell-editor";
18
18
  import { SpreadsheetColumnHeader } from "./table-column-header";
19
19
  import { TableColumnSheet } from "./table-column-sheet";
20
20
  import { TableToolbar } from "./table-toolbar";
21
21
  import { TableImportWizard } from "./table-import-wizard";
22
+ import { TableRowSheet } from "./table-row-sheet";
22
23
  import { EmptyState } from "@/components/shared/empty-state";
23
24
  import { Table2 } from "lucide-react";
24
- import {
25
- useSpreadsheetKeys,
26
- type CellPosition,
27
- } from "./use-spreadsheet-keys";
28
25
  import { evaluateComputedColumns } from "@/lib/tables/computed";
29
26
  import type { ColumnDef, SortSpec } from "@/lib/tables/types";
30
27
  import type { UserTableRowRow } from "@/lib/db/schema";
@@ -58,15 +55,12 @@ export function TableSpreadsheet({
58
55
  }: TableSpreadsheetProps) {
59
56
  const [columns, setColumns] = useState<ColumnDef[]>(initialColumns);
60
57
  const [rows, setRows] = useState<ParsedRow[]>(() => parseRows(initialRows));
61
- const [editingCell, setEditingCell] = useState<CellPosition | null>(null);
62
- const [editValue, setEditValue] = useState<unknown>(null);
63
58
  const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
64
59
  const [sorts, setSorts] = useState<SortSpec[]>([]);
65
60
  const [columnSheetOpen, setColumnSheetOpen] = useState(false);
66
61
  const [importOpen, setImportOpen] = useState(false);
67
- const [pendingSaves, setPendingSaves] = useState<Set<string>>(new Set());
68
-
69
- const debounceTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
62
+ const [rowSheetOpen, setRowSheetOpen] = useState(false);
63
+ const [rowSheetRow, setRowSheetRow] = useState<ParsedRow | null>(null);
70
64
 
71
65
  // ── Refresh helpers ─────────────────────────────────────────────────
72
66
 
@@ -87,116 +81,33 @@ export function TableSpreadsheet({
87
81
  const res = await fetch(`/api/tables/${tableId}/rows?limit=500${sortsParam}`);
88
82
  if (res.ok) {
89
83
  const raw = (await res.json()) as UserTableRowRow[];
90
- // Evaluate computed columns client-side after refresh
91
84
  const enriched = evaluateComputedColumns(columns, raw);
92
85
  setRows(parseRows(enriched));
93
86
  }
94
87
  } catch { /* silent */ }
95
88
  }, [tableId, sorts, columns]);
96
89
 
97
- // ── Cell operations ─────────────────────────────────────────────────
98
-
99
- const getCellValue = useCallback(
100
- (pos: CellPosition): unknown => {
101
- const row = rows[pos.rowIndex];
102
- const col = columns[pos.colIndex];
103
- if (!row || !col) return null;
104
- return row.data[col.name] ?? null;
105
- },
106
- [rows, columns]
107
- );
90
+ // ── Boolean toggle (inline for convenience) ─────────────────────────
108
91
 
109
- const setCellValue = useCallback(
110
- (rowId: string, colName: string, value: unknown) => {
92
+ const handleToggleBoolean = useCallback(
93
+ (rowId: string, colName: string, newValue: boolean) => {
111
94
  setRows((prev) =>
112
95
  prev.map((r) =>
113
96
  r.id === rowId
114
- ? { ...r, data: { ...r.data, [colName]: value } }
97
+ ? { ...r, data: { ...r.data, [colName]: newValue } }
115
98
  : r
116
99
  )
117
100
  );
118
- },
119
- []
120
- );
121
-
122
- const debounceSave = useCallback(
123
- (rowId: string, colName: string, value: unknown) => {
124
- const key = `${rowId}:${colName}`;
125
-
126
- // Clear previous timer
127
- const existing = debounceTimers.current.get(key);
128
- if (existing) clearTimeout(existing);
129
-
130
- setPendingSaves((prev) => new Set(prev).add(key));
131
-
132
- const timer = setTimeout(async () => {
133
- try {
134
- const res = await fetch(`/api/tables/${tableId}/rows/${rowId}`, {
135
- method: "PATCH",
136
- headers: { "Content-Type": "application/json" },
137
- body: JSON.stringify({ data: { [colName]: value } }),
138
- });
139
- if (!res.ok) {
140
- toast.error("Failed to save cell");
141
- // Revert would require storing previous value — for now just notify
142
- }
143
- } catch {
144
- toast.error("Failed to save cell");
145
- } finally {
146
- setPendingSaves((prev) => {
147
- const next = new Set(prev);
148
- next.delete(key);
149
- return next;
150
- });
151
- debounceTimers.current.delete(key);
152
- }
153
- }, 300);
154
-
155
- debounceTimers.current.set(key, timer);
101
+ // Save immediately
102
+ fetch(`/api/tables/${tableId}/rows/${rowId}`, {
103
+ method: "PATCH",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ data: { [colName]: newValue } }),
106
+ }).catch(() => toast.error("Failed to save"));
156
107
  },
157
108
  [tableId]
158
109
  );
159
110
 
160
- // ── Keyboard hook callbacks ─────────────────────────────────────────
161
-
162
- const handleStartEdit = useCallback(
163
- (pos: CellPosition) => {
164
- setEditingCell(pos);
165
- setEditValue(getCellValue(pos));
166
- },
167
- [getCellValue]
168
- );
169
-
170
- const handleConfirmEdit = useCallback(() => {
171
- if (!editingCell) return;
172
- const row = rows[editingCell.rowIndex];
173
- const col = columns[editingCell.colIndex];
174
- if (!row || !col) return;
175
-
176
- setCellValue(row.id, col.name, editValue);
177
- debounceSave(row.id, col.name, editValue);
178
- setEditingCell(null);
179
- setEditValue(null);
180
- }, [editingCell, editValue, rows, columns, setCellValue, debounceSave]);
181
-
182
- const handleCancelEdit = useCallback(() => {
183
- setEditingCell(null);
184
- setEditValue(null);
185
- }, []);
186
-
187
- const handleClearCell = useCallback(
188
- (pos: CellPosition) => {
189
- const row = rows[pos.rowIndex];
190
- const col = columns[pos.colIndex];
191
- if (!row || !col) return;
192
- if (col.dataType === "computed") return;
193
-
194
- setCellValue(row.id, col.name, null);
195
- debounceSave(row.id, col.name, null);
196
- },
197
- [rows, columns, setCellValue, debounceSave]
198
- );
199
-
200
111
  // ── Row operations ──────────────────────────────────────────────────
201
112
 
202
113
  const handleAddRow = useCallback(async () => {
@@ -238,6 +149,19 @@ export function TableSpreadsheet({
238
149
  }
239
150
  }, [tableId, selectedRows, refreshRows]);
240
151
 
152
+ // ── Row sheet ──────────────────────────────────────────────────────
153
+
154
+ const handleOpenRowSheet = useCallback((row: ParsedRow) => {
155
+ setRowSheetRow(row);
156
+ setRowSheetOpen(true);
157
+ }, []);
158
+
159
+ const handleRowUpdated = useCallback((rowId: string, data: Record<string, unknown>) => {
160
+ setRows((prev) =>
161
+ prev.map((r) => (r.id === rowId ? { ...r, data } : r))
162
+ );
163
+ }, []);
164
+
241
165
  // ── Column operations ───────────────────────────────────────────────
242
166
 
243
167
  const handleSort = useCallback(
@@ -261,18 +185,6 @@ export function TableSpreadsheet({
261
185
  refreshRows();
262
186
  }
263
187
 
264
- // ── Keyboard navigation ─────────────────────────────────────────────
265
-
266
- const keyboard = useSpreadsheetKeys({
267
- rowCount: rows.length,
268
- colCount: columns.length,
269
- onStartEdit: handleStartEdit,
270
- onConfirmEdit: handleConfirmEdit,
271
- onCancelEdit: handleCancelEdit,
272
- onClearCell: handleClearCell,
273
- onAddRow: handleAddRow,
274
- });
275
-
276
188
  // ── Selection helpers ───────────────────────────────────────────────
277
189
 
278
190
  function toggleRowSelect(rowId: string) {
@@ -328,7 +240,7 @@ export function TableSpreadsheet({
328
240
  }
329
241
 
330
242
  return (
331
- <div className="space-y-2" data-spreadsheet>
243
+ <div className="space-y-2">
332
244
  <TableToolbar
333
245
  tableId={tableId}
334
246
  rowCount={rows.length}
@@ -392,12 +304,14 @@ export function TableSpreadsheet({
392
304
  </TableCell>
393
305
  </TableRow>
394
306
  ) : (
395
- rows.map((row, rowIndex) => (
307
+ rows.map((row) => (
396
308
  <TableRow
397
309
  key={row.id}
398
310
  className={cn(
311
+ "cursor-pointer",
399
312
  selectedRows.has(row.id) && "bg-muted/50"
400
313
  )}
314
+ onClick={() => handleOpenRowSheet(row)}
401
315
  >
402
316
  <TableCell onClick={(e) => e.stopPropagation()}>
403
317
  <Checkbox
@@ -405,56 +319,26 @@ export function TableSpreadsheet({
405
319
  onCheckedChange={() => toggleRowSelect(row.id)}
406
320
  />
407
321
  </TableCell>
408
- {columns.map((col, colIndex) => {
409
- const isActive =
410
- keyboard.activeCell?.rowIndex === rowIndex &&
411
- keyboard.activeCell?.colIndex === colIndex;
412
- const isEditing =
413
- isActive &&
414
- editingCell?.rowIndex === rowIndex &&
415
- editingCell?.colIndex === colIndex;
322
+ {columns.map((col) => {
416
323
  const value = row.data[col.name];
417
- const saveKey = `${row.id}:${col.name}`;
418
- const isSaving = pendingSaves.has(saveKey);
419
324
 
420
325
  return (
421
326
  <TableCell
422
327
  key={col.name}
423
- className={cn(
424
- "p-0 cursor-cell relative min-w-[120px]",
425
- isActive && "ring-2 ring-primary ring-inset",
426
- isSaving && "bg-primary/5"
427
- )}
428
- onClick={() =>
429
- keyboard.handleCellClick({ rowIndex, colIndex })
430
- }
431
- onDoubleClick={() =>
432
- keyboard.handleCellDoubleClick({ rowIndex, colIndex })
433
- }
328
+ className="min-w-[120px]"
434
329
  >
435
330
  <div className="px-2 py-1 min-h-[32px] flex items-center">
436
- {isEditing && col.dataType !== "computed" ? (
437
- <CellEditor
438
- column={col}
439
- value={editValue}
440
- onChange={setEditValue}
441
- onConfirm={handleConfirmEdit}
442
- onCancel={handleCancelEdit}
443
- />
444
- ) : (
445
- <CellDisplay
446
- column={col}
447
- value={value}
448
- onToggleBoolean={
449
- col.dataType === "boolean"
450
- ? (newVal) => {
451
- setCellValue(row.id, col.name, newVal);
452
- debounceSave(row.id, col.name, newVal);
453
- }
454
- : undefined
455
- }
456
- />
457
- )}
331
+ <CellDisplay
332
+ column={col}
333
+ value={value}
334
+ onToggleBoolean={
335
+ col.dataType === "boolean"
336
+ ? (newVal) => {
337
+ handleToggleBoolean(row.id, col.name, newVal);
338
+ }
339
+ : undefined
340
+ }
341
+ />
458
342
  </div>
459
343
  </TableCell>
460
344
  );
@@ -494,6 +378,17 @@ export function TableSpreadsheet({
494
378
  onOpenChange={setImportOpen}
495
379
  onImported={() => { refreshTable(); refreshRows(); }}
496
380
  />
381
+
382
+ {rowSheetRow && (
383
+ <TableRowSheet
384
+ tableId={tableId}
385
+ columns={columns}
386
+ row={rowSheetRow}
387
+ open={rowSheetOpen}
388
+ onOpenChange={setRowSheetOpen}
389
+ onRowUpdated={handleRowUpdated}
390
+ />
391
+ )}
497
392
  </div>
498
393
  );
499
394
  }
@@ -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
 
@@ -472,7 +472,7 @@ export function WorkflowFormView({
472
472
  ? normalizeParallelSteps(def.steps)
473
473
  : def.pattern === "swarm"
474
474
  ? normalizeSwarmSteps(def.steps)
475
- : def.steps
475
+ : def.steps.map((s) => ({ ...s, id: s.id || crypto.randomUUID() }))
476
476
  );
477
477
  }
478
478
  }
@@ -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:
@@ -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
+ }