@stackbilt/aegis-core 0.1.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 (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,116 @@
1
+ // Bluesky API routes — direct AT Protocol operations via stored credentials
2
+ // All endpoints require bearer auth (same as other /api/* routes).
3
+
4
+ import { Hono } from 'hono';
5
+ import { bodyLimit } from 'hono/body-limit';
6
+ import type { Env } from '../types.js';
7
+ import {
8
+ postToBluesky,
9
+ getAuthorFeed,
10
+ likePost,
11
+ repostPost,
12
+ deleteBlueskyPost,
13
+ getNotifications,
14
+ } from '../bluesky.js';
15
+
16
+ const BLUESKY_BODY_LIMIT = 100 * 1024;
17
+
18
+ const bluesky = new Hono<{ Bindings: Env }>();
19
+
20
+ function getCredentials(env: Env) {
21
+ const handle = env.BLUESKY_HANDLE;
22
+ const appPassword = env.BLUESKY_APP_PASSWORD;
23
+ if (!handle || !appPassword) {
24
+ throw new Error('BLUESKY_HANDLE and BLUESKY_APP_PASSWORD must be configured');
25
+ }
26
+ return { handle, appPassword };
27
+ }
28
+
29
+ // POST /api/bluesky/post — create a new post
30
+ bluesky.post('/api/bluesky/post', bodyLimit({ maxSize: BLUESKY_BODY_LIMIT }), async (c) => {
31
+ try {
32
+ const { handle, appPassword } = getCredentials(c.env);
33
+ const body = await c.req.json<{
34
+ text: string;
35
+ image_url?: string;
36
+ image_alt?: string;
37
+ link_url?: string;
38
+ langs?: string[];
39
+ }>();
40
+
41
+ if (!body.text) return c.json({ error: 'text is required' }, 400);
42
+
43
+ const result = await postToBluesky(handle, appPassword, {
44
+ text: body.text,
45
+ imageUrl: body.image_url,
46
+ imageAlt: body.image_alt,
47
+ langs: body.langs,
48
+ });
49
+
50
+ // Record in content_queue as published (live DB uses `content`/`media_url` columns)
51
+ const id = crypto.randomUUID();
52
+ await c.env.DB.prepare(`
53
+ INSERT INTO content_queue (id, platform, content, media_url, link_url, scheduled_at, status, published_at, post_url)
54
+ VALUES (?, 'bluesky', ?, ?, ?, datetime('now'), 'published', datetime('now'), ?)
55
+ `).bind(id, body.text, body.image_url ?? null, body.link_url ?? null, result.url).run();
56
+
57
+ return c.json({ uri: result.uri, cid: result.cid, url: result.url, queue_id: id });
58
+ } catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ console.error('[bluesky/post]', msg);
61
+ return c.json({ error: msg }, 500);
62
+ }
63
+ });
64
+
65
+ // GET /api/bluesky/feed — get our recent posts
66
+ bluesky.get('/api/bluesky/feed', async (c) => {
67
+ const handle = c.env.BLUESKY_HANDLE || 'your-handle.bsky.social';
68
+ const limit = Math.min(Number(c.req.query('limit') ?? 20), 100);
69
+
70
+ const posts = await getAuthorFeed(handle, limit);
71
+ return c.json({ posts, count: posts.length });
72
+ });
73
+
74
+ // POST /api/bluesky/like — like a post
75
+ bluesky.post('/api/bluesky/like', bodyLimit({ maxSize: BLUESKY_BODY_LIMIT }), async (c) => {
76
+ const { handle, appPassword } = getCredentials(c.env);
77
+ const body = await c.req.json<{ uri: string; cid: string }>();
78
+
79
+ if (!body.uri || !body.cid) return c.json({ error: 'uri and cid are required' }, 400);
80
+
81
+ const result = await likePost(handle, appPassword, body.uri, body.cid);
82
+ return c.json(result);
83
+ });
84
+
85
+ // POST /api/bluesky/repost — repost a post
86
+ bluesky.post('/api/bluesky/repost', bodyLimit({ maxSize: BLUESKY_BODY_LIMIT }), async (c) => {
87
+ const { handle, appPassword } = getCredentials(c.env);
88
+ const body = await c.req.json<{ uri: string; cid: string }>();
89
+
90
+ if (!body.uri || !body.cid) return c.json({ error: 'uri and cid are required' }, 400);
91
+
92
+ const result = await repostPost(handle, appPassword, body.uri, body.cid);
93
+ return c.json(result);
94
+ });
95
+
96
+ // DELETE /api/bluesky/post — delete a post
97
+ bluesky.delete('/api/bluesky/post', async (c) => {
98
+ const { handle, appPassword } = getCredentials(c.env);
99
+ const body = await c.req.json<{ uri: string }>();
100
+
101
+ if (!body.uri) return c.json({ error: 'uri is required' }, 400);
102
+
103
+ await deleteBlueskyPost(handle, appPassword, body.uri);
104
+ return c.json({ deleted: true, uri: body.uri });
105
+ });
106
+
107
+ // GET /api/bluesky/notifications — check notifications
108
+ bluesky.get('/api/bluesky/notifications', async (c) => {
109
+ const { handle, appPassword } = getCredentials(c.env);
110
+ const limit = Math.min(Number(c.req.query('limit') ?? 30), 100);
111
+
112
+ const notifications = await getNotifications(handle, appPassword, limit);
113
+ return c.json({ notifications, count: notifications.length });
114
+ });
115
+
116
+ export { bluesky };
@@ -0,0 +1,328 @@
1
+ import { Hono } from 'hono';
2
+ import { bodyLimit } from 'hono/body-limit';
3
+ import type { Env } from '../types.js';
4
+ import { classifyTaskFailure, parseTaskPreflight, scoreTaskUtility } from '../task-intelligence.js';
5
+ import { moveBoardItemLocal, linkTaskToBoard } from '../kernel/board.js';
6
+ import { TASK_AUTHORITIES, TASK_CATEGORIES, validateEnum } from '../schema-enums.js';
7
+
8
+ const CC_TASKS_BODY_LIMIT = 256 * 1024;
9
+
10
+ const ccTasks = new Hono<{ Bindings: Env }>();
11
+
12
+ // Get next pending task (respects dependencies and priority)
13
+ ccTasks.get('/api/cc-tasks/next', async (c) => {
14
+ const task = await c.env.DB.prepare(`
15
+ SELECT * FROM cc_tasks
16
+ WHERE status = 'pending'
17
+ AND authority != 'proposed'
18
+ AND (depends_on IS NULL OR depends_on IN (
19
+ SELECT id FROM cc_tasks WHERE status = 'completed'
20
+ ))
21
+ AND (blocked_by IS NULL OR NOT EXISTS (
22
+ SELECT 1 FROM json_each(blocked_by) AS b
23
+ WHERE b.value NOT IN (SELECT id FROM cc_tasks WHERE status = 'completed')
24
+ ))
25
+ AND repo NOT IN (
26
+ SELECT DISTINCT repo FROM cc_tasks WHERE status = 'running'
27
+ )
28
+ ORDER BY priority ASC, created_at ASC
29
+ LIMIT 1
30
+ `).first();
31
+
32
+ if (!task) return c.json({ id: null, message: 'Queue empty' });
33
+ return c.json(task);
34
+ });
35
+
36
+ // List tasks with optional status + business_unit filter
37
+ ccTasks.get('/api/cc-tasks', async (c) => {
38
+ const status = c.req.query('status');
39
+ const businessUnit = c.req.query('business_unit');
40
+ const limit = Math.min(parseInt(c.req.query('limit') ?? '50', 10), 100);
41
+
42
+ const conditions: string[] = [];
43
+ const bindings: unknown[] = [];
44
+ if (status) {
45
+ conditions.push('status = ?');
46
+ bindings.push(status);
47
+ }
48
+ if (businessUnit) {
49
+ conditions.push('business_unit = ?');
50
+ bindings.push(businessUnit);
51
+ }
52
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
53
+ const query = `SELECT * FROM cc_tasks ${where} ORDER BY created_at DESC LIMIT ?`;
54
+ bindings.push(limit);
55
+
56
+ const tasks = await c.env.DB.prepare(query).bind(...bindings).all();
57
+ return c.json({ count: tasks.results.length, tasks: tasks.results });
58
+ });
59
+
60
+ // Create a new task
61
+ ccTasks.post('/api/cc-tasks', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
62
+ const body = await c.req.json<{
63
+ title: string;
64
+ repo: string;
65
+ prompt: string;
66
+ completion_signal?: string;
67
+ priority?: number;
68
+ depends_on?: string;
69
+ blocked_by?: string[];
70
+ max_turns?: number;
71
+ allowed_tools?: string[];
72
+ created_by?: string;
73
+ authority?: string;
74
+ category?: string;
75
+ github_issue_repo?: string;
76
+ github_issue_number?: number;
77
+ business_unit?: string;
78
+ }>();
79
+
80
+ if (!body.title?.trim() || !body.repo?.trim() || !body.prompt?.trim()) {
81
+ return c.json({ error: 'title, repo, and prompt are required' }, 400);
82
+ }
83
+
84
+ const authority = validateEnum(TASK_AUTHORITIES, body.authority, 'operator');
85
+ const category = validateEnum(TASK_CATEGORIES, body.category, 'feature');
86
+ const blockedBy = body.blocked_by?.length ? body.blocked_by : null;
87
+
88
+ // Cycle detection: ensure none of the blockers are blocked by this task (direct cycle)
89
+ if (blockedBy) {
90
+ const id_placeholder = crypto.randomUUID(); // preview ID for check
91
+ const cycleCheck = await c.env.DB.prepare(`
92
+ SELECT id FROM cc_tasks
93
+ WHERE id IN (${blockedBy.map(() => '?').join(',')})
94
+ AND (depends_on = ? OR (blocked_by IS NOT NULL AND EXISTS (
95
+ SELECT 1 FROM json_each(blocked_by) AS b WHERE b.value = ?
96
+ )))
97
+ `).bind(...blockedBy, id_placeholder, id_placeholder).first();
98
+ // Note: full transitive cycle detection deferred — direct cycles caught here
99
+ void cycleCheck; // blockers can't reference a task that doesn't exist yet, so this is safe for creation
100
+ }
101
+
102
+ const id = crypto.randomUUID();
103
+ await c.env.DB.prepare(`
104
+ INSERT INTO cc_tasks (id, title, repo, prompt, completion_signal, priority, depends_on, blocked_by, max_turns, allowed_tools, created_by, authority, category, github_issue_repo, github_issue_number, business_unit)
105
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
106
+ `).bind(
107
+ id,
108
+ body.title.trim(),
109
+ body.repo.trim(),
110
+ body.prompt.trim(),
111
+ body.completion_signal ?? null,
112
+ body.priority ?? 50,
113
+ body.depends_on ?? null,
114
+ blockedBy ? JSON.stringify(blockedBy) : null,
115
+ body.max_turns ?? 25,
116
+ body.allowed_tools ? JSON.stringify(body.allowed_tools) : null,
117
+ body.created_by ?? 'operator',
118
+ authority,
119
+ category,
120
+ body.github_issue_repo ?? null,
121
+ body.github_issue_number ?? null,
122
+ body.business_unit?.trim() || 'stackbilt',
123
+ ).run();
124
+
125
+ return c.json({ id, status: 'pending', authority, category }, 201);
126
+ });
127
+
128
+ // Mark task as started
129
+ ccTasks.post('/api/cc-tasks/:id/start', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
130
+ const taskId = c.req.param('id');
131
+ const body = await c.req.json<{ session_id?: string; preflight?: Record<string, unknown> }>()
132
+ .catch(() => ({ session_id: undefined, preflight: undefined }));
133
+ const preflightJson = body.preflight ? JSON.stringify(body.preflight) : null;
134
+
135
+ await c.env.DB.prepare(`
136
+ UPDATE cc_tasks SET status = 'running', session_id = ?, preflight_json = COALESCE(?, preflight_json), started_at = datetime('now')
137
+ WHERE id = ? AND status = 'pending'
138
+ `).bind(body.session_id ?? null, preflightJson, taskId).run();
139
+
140
+ // Update board item if linked to a GitHub issue
141
+ const task = await c.env.DB.prepare(
142
+ 'SELECT github_issue_repo, github_issue_number FROM cc_tasks WHERE id = ?',
143
+ ).bind(taskId).first<{ github_issue_repo: string | null; github_issue_number: number | null }>();
144
+ if (task?.github_issue_repo && task.github_issue_number) {
145
+ await moveBoardItemLocal(c.env.DB, task.github_issue_repo, task.github_issue_number, 'in_progress').catch(() => {});
146
+ }
147
+
148
+ return c.json({ id: taskId, status: 'running' });
149
+ });
150
+
151
+ // Mark task as completed/failed
152
+ ccTasks.post('/api/cc-tasks/:id/complete', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
153
+ const taskId = c.req.param('id');
154
+ const body = await c.req.json<{
155
+ status: 'completed' | 'failed';
156
+ result?: string;
157
+ error?: string;
158
+ exit_code?: number;
159
+ pr_url?: string;
160
+ branch?: string;
161
+ task_title?: string;
162
+ repo?: string;
163
+ category?: string;
164
+ preflight?: Record<string, unknown>;
165
+ }>();
166
+
167
+ const status = body.status === 'completed' ? 'completed' : 'failed';
168
+ const preflight = parseTaskPreflight(body.preflight ?? null);
169
+ const preflightJson = preflight ? JSON.stringify(preflight) : null;
170
+ const autopsy = status === 'failed'
171
+ ? classifyTaskFailure({
172
+ title: body.task_title ?? null,
173
+ repo: body.repo ?? null,
174
+ category: body.category ?? null,
175
+ error: body.error ?? null,
176
+ result: body.result ?? null,
177
+ exitCode: body.exit_code ?? null,
178
+ preflight,
179
+ })
180
+ : null;
181
+
182
+ // PR utility scoring for completed tasks (#289)
183
+ let utilityJson: string | null = null;
184
+ if (status === 'completed') {
185
+ // Fetch recent autonomous task titles for novelty comparison
186
+ const recentAuto = await c.env.DB.prepare(`
187
+ SELECT title FROM cc_tasks
188
+ WHERE status = 'completed' AND created_by != 'operator'
189
+ AND completed_at > datetime('now', '-14 days') AND id != ?
190
+ ORDER BY completed_at DESC LIMIT 20
191
+ `).bind(taskId).all<{ title: string }>();
192
+
193
+ // Fetch task metadata for scoring (created_by, github_issue_number)
194
+ const taskMeta = await c.env.DB.prepare(
195
+ 'SELECT created_by, github_issue_number, category FROM cc_tasks WHERE id = ?',
196
+ ).bind(taskId).first<{ created_by: string; github_issue_number: number | null; category: string }>();
197
+
198
+ const utility = scoreTaskUtility({
199
+ title: body.task_title ?? '',
200
+ category: taskMeta?.category ?? body.category ?? 'feature',
201
+ result: body.result ?? null,
202
+ created_by: taskMeta?.created_by ?? 'operator',
203
+ pr_url: body.pr_url ?? null,
204
+ github_issue_number: taskMeta?.github_issue_number ?? null,
205
+ recentAutoTitles: recentAuto.results.map(r => r.title),
206
+ });
207
+
208
+ utilityJson = JSON.stringify(utility);
209
+ console.log(`[utility] task ${taskId.slice(0, 8)}: impact=${utility.impact} novelty=${utility.novelty} signals=${utility.signals.length}`);
210
+ }
211
+
212
+ await c.env.DB.prepare(`
213
+ UPDATE cc_tasks
214
+ SET status = ?, result = ?, error = ?, exit_code = ?, pr_url = ?, branch = ?,
215
+ preflight_json = COALESCE(?, preflight_json), failure_kind = ?, retryable = ?,
216
+ autopsy_json = ?, utility_json = ?, completed_at = datetime('now')
217
+ WHERE id = ?
218
+ `).bind(
219
+ status,
220
+ body.result ?? null,
221
+ body.error ?? null,
222
+ body.exit_code ?? null,
223
+ body.pr_url ?? null,
224
+ body.branch ?? null,
225
+ preflightJson,
226
+ autopsy?.kind ?? null,
227
+ autopsy?.retryable ? 1 : 0,
228
+ autopsy ? JSON.stringify(autopsy) : null,
229
+ utilityJson,
230
+ taskId,
231
+ ).run();
232
+
233
+ // Update board item if linked to a GitHub issue
234
+ const completedTask = await c.env.DB.prepare(
235
+ 'SELECT github_issue_repo, github_issue_number, retryable FROM cc_tasks WHERE id = ?',
236
+ ).bind(taskId).first<{ github_issue_repo: string | null; github_issue_number: number | null; retryable: number | null }>();
237
+ if (completedTask?.github_issue_repo && completedTask.github_issue_number) {
238
+ const boardStatus = status === 'completed'
239
+ ? 'shipped' as const
240
+ : (completedTask.retryable ? 'queued' as const : 'blocked' as const);
241
+ await moveBoardItemLocal(c.env.DB, completedTask.github_issue_repo, completedTask.github_issue_number, boardStatus).catch(() => {});
242
+ }
243
+
244
+ // Cascade cancel: when a task fails, cancel all pending tasks that depend on it
245
+ // Checks both depends_on (single) and blocked_by (DAG array)
246
+ if (status === 'failed') {
247
+ const cancelReason = `Dependency ${taskId} failed`;
248
+
249
+ // Cancel tasks using depends_on (legacy single-dependency)
250
+ await c.env.DB.prepare(`
251
+ UPDATE cc_tasks SET status = 'cancelled', error = ?
252
+ WHERE depends_on = ? AND status = 'pending'
253
+ `).bind(cancelReason, taskId).run();
254
+
255
+ // Cancel tasks using blocked_by (DAG multi-dependency)
256
+ await c.env.DB.prepare(`
257
+ UPDATE cc_tasks SET status = 'cancelled', error = ?
258
+ WHERE status = 'pending' AND blocked_by IS NOT NULL
259
+ AND EXISTS (SELECT 1 FROM json_each(blocked_by) AS b WHERE b.value = ?)
260
+ `).bind(cancelReason, taskId).run();
261
+
262
+ // Recurse one level: cancel tasks depending on the just-cancelled ones
263
+ const nowCancelled = await c.env.DB.prepare(`
264
+ SELECT id FROM cc_tasks WHERE status = 'cancelled' AND error = ?
265
+ `).bind(cancelReason).all();
266
+
267
+ for (const dep of nowCancelled.results) {
268
+ const depId = (dep as any).id;
269
+ const upstreamReason = `Dependency ${depId} failed (upstream: ${taskId})`;
270
+ await c.env.DB.prepare(`
271
+ UPDATE cc_tasks SET status = 'cancelled', error = ?
272
+ WHERE status = 'pending' AND (
273
+ depends_on = ?
274
+ OR (blocked_by IS NOT NULL AND EXISTS (
275
+ SELECT 1 FROM json_each(blocked_by) AS b WHERE b.value = ?
276
+ ))
277
+ )
278
+ `).bind(upstreamReason, depId, depId).run();
279
+ }
280
+ }
281
+
282
+ return c.json({
283
+ id: taskId,
284
+ status,
285
+ failure_kind: autopsy?.kind ?? null,
286
+ retryable: autopsy?.retryable ?? false,
287
+ });
288
+ });
289
+
290
+ // Cancel a pending or running task
291
+ ccTasks.post('/api/cc-tasks/:id/cancel', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
292
+ const taskId = c.req.param('id');
293
+ const result = await c.env.DB.prepare(`
294
+ UPDATE cc_tasks SET status = 'cancelled', completed_at = datetime('now') WHERE id = ? AND status IN ('pending', 'running')
295
+ `).bind(taskId).run();
296
+ if (!result.meta.changes) {
297
+ return c.json({ error: 'Task not found or not in a cancellable status (pending/running)' }, 404);
298
+ }
299
+ return c.json({ id: taskId, status: 'cancelled' });
300
+ });
301
+
302
+ // Approve a proposed task (makes it eligible for execution)
303
+ ccTasks.post('/api/cc-tasks/:id/approve', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
304
+ const taskId = c.req.param('id');
305
+ const result = await c.env.DB.prepare(`
306
+ UPDATE cc_tasks SET authority = 'operator'
307
+ WHERE id = ? AND authority = 'proposed' AND status = 'pending'
308
+ `).bind(taskId).run();
309
+ if (!result.meta.changes) {
310
+ return c.json({ error: 'Task not found, not proposed, or not pending' }, 404);
311
+ }
312
+ return c.json({ id: taskId, authority: 'operator', status: 'pending' });
313
+ });
314
+
315
+ // Reject a proposed task
316
+ ccTasks.post('/api/cc-tasks/:id/reject', bodyLimit({ maxSize: CC_TASKS_BODY_LIMIT }), async (c) => {
317
+ const taskId = c.req.param('id');
318
+ const result = await c.env.DB.prepare(`
319
+ UPDATE cc_tasks SET status = 'cancelled'
320
+ WHERE id = ? AND authority = 'proposed' AND status = 'pending'
321
+ `).bind(taskId).run();
322
+ if (!result.meta.changes) {
323
+ return c.json({ error: 'Task not found, not proposed, or not pending' }, 404);
324
+ }
325
+ return c.json({ id: taskId, status: 'cancelled' });
326
+ });
327
+
328
+ export { ccTasks };
@@ -0,0 +1 @@
1
+ export { codebeast } from '../codebeast.js';
@@ -0,0 +1,194 @@
1
+ // Stub — full implementation not yet extracted to OSS
2
+
3
+ import { Hono } from 'hono';
4
+ import { bodyLimit } from 'hono/body-limit';
5
+ import type { Env } from '../types.js';
6
+ import { sanitizeForBlog } from '../sanitize.js';
7
+ import { buildEdgeEnv } from '../edge-env.js';
8
+ import { runRoundtableGeneration, runJournalGeneration } from '../content/index.js';
9
+ import { runDreamingCycle } from '../kernel/scheduled/dreaming.js';
10
+ import { operatorConfig } from '../operator/index.js';
11
+
12
+ const TECH_POSTS_BODY_LIMIT = 256 * 1024;
13
+ const DEFAULT_BODY_LIMIT = 100 * 1024;
14
+
15
+ const content = new Hono<{ Bindings: Env }>();
16
+
17
+ const BLOG_BASE_URL = 'https://your-blog.example.com';
18
+
19
+ // ─── Tech Blog Redirects ────────────────────────────────────
20
+
21
+ content.get('/tech', (c) => {
22
+ return c.redirect(BLOG_BASE_URL, 301);
23
+ });
24
+
25
+ content.get('/tech/feed.xml', (c) => {
26
+ return c.redirect(`${BLOG_BASE_URL}/feed.xml`, 301);
27
+ });
28
+
29
+ content.get('/tech/:slug', (c) => {
30
+ return c.redirect(`${BLOG_BASE_URL}/post/${c.req.param('slug')}`, 301);
31
+ });
32
+
33
+ // ─── Tech Post CRUD ─────────────────────────────────────────
34
+
35
+ content.get('/api/tech-posts', async (c) => {
36
+ const roundtableDb = (c.env as any).ROUNDTABLE_DB;
37
+ if (!roundtableDb) return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
38
+
39
+ const status = c.req.query('status');
40
+ let query: string;
41
+ const bindings: unknown[] = [];
42
+
43
+ if (status) {
44
+ query = 'SELECT * FROM posts WHERE status = ? ORDER BY created_at DESC';
45
+ bindings.push(status);
46
+ } else {
47
+ query = 'SELECT * FROM posts ORDER BY created_at DESC';
48
+ }
49
+
50
+ const stmt = roundtableDb.prepare(query);
51
+ const result = bindings.length > 0 ? await stmt.bind(...bindings).all() : await stmt.all();
52
+
53
+ return c.json({ posts: result.results });
54
+ });
55
+
56
+ content.post('/api/tech-posts', bodyLimit({ maxSize: TECH_POSTS_BODY_LIMIT }), async (c) => {
57
+ const roundtableDb = (c.env as any).ROUNDTABLE_DB;
58
+ if (!roundtableDb) return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
59
+
60
+ const body = await c.req.json<{
61
+ title?: string;
62
+ slug?: string;
63
+ body?: string;
64
+ description?: string;
65
+ tags?: string[];
66
+ status?: string;
67
+ canonical_url?: string;
68
+ }>();
69
+
70
+ if (!body.title?.trim() || !body.slug?.trim() || !body.body?.trim()) {
71
+ return c.json({ error: 'title, slug, and body are required' }, 400);
72
+ }
73
+
74
+ const id = crypto.randomUUID();
75
+ const status = body.status === 'published' ? 'published' : 'draft';
76
+ const publishedAt = status === 'published' ? new Date().toISOString() : null;
77
+
78
+ await roundtableDb.prepare(
79
+ `INSERT INTO posts (id, title, slug, body, description, tags, status, canonical_url, published_at)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
81
+ ).bind(
82
+ id,
83
+ body.title.trim(),
84
+ body.slug.trim(),
85
+ body.body.trim(),
86
+ body.description ?? '',
87
+ JSON.stringify(body.tags ?? []),
88
+ status,
89
+ body.canonical_url ?? null,
90
+ publishedAt,
91
+ ).run();
92
+
93
+ return c.json({
94
+ id,
95
+ slug: body.slug.trim(),
96
+ status,
97
+ url: status === 'published' ? `${BLOG_BASE_URL}/post/${body.slug.trim()}` : null,
98
+ note: status === 'draft' ? 'No public route exists for draft posts.' : undefined,
99
+ }, 201);
100
+ });
101
+
102
+ content.put('/api/tech-posts/:id', async (c) => {
103
+ const roundtableDb = (c.env as any).ROUNDTABLE_DB;
104
+ if (!roundtableDb) return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
105
+
106
+ const postId = c.req.param('id');
107
+ const existing = await (roundtableDb as D1Database).prepare(
108
+ 'SELECT * FROM posts WHERE id = ?'
109
+ ).bind(postId).first() as any;
110
+
111
+ if (!existing) return c.json({ error: 'Post not found' }, 404);
112
+
113
+ const body = await c.req.json<{
114
+ title?: string;
115
+ body?: string;
116
+ description?: string;
117
+ tags?: string[];
118
+ status?: string;
119
+ canonical_url?: string;
120
+ }>();
121
+
122
+ const newStatus = body.status ?? existing.status;
123
+ let publishedAt = existing.published_at;
124
+ if (newStatus === 'published' && !existing.published_at) {
125
+ publishedAt = new Date().toISOString();
126
+ }
127
+
128
+ await roundtableDb.prepare(
129
+ `UPDATE posts SET title = ?, body = ?, description = ?, tags = ?, status = ?, canonical_url = ?, published_at = ?
130
+ WHERE id = ?`
131
+ ).bind(
132
+ body.title ?? existing.title,
133
+ body.body ?? existing.body,
134
+ body.description ?? existing.description,
135
+ JSON.stringify(body.tags ?? JSON.parse(existing.tags ?? '[]')),
136
+ newStatus,
137
+ body.canonical_url ?? existing.canonical_url,
138
+ publishedAt,
139
+ postId,
140
+ ).run();
141
+
142
+ return c.json({ ok: true });
143
+ });
144
+
145
+ // ─── Content Generation Triggers ────────────────────────────
146
+
147
+ content.post('/api/generate/roundtable', bodyLimit({ maxSize: DEFAULT_BODY_LIMIT }), async (c) => {
148
+ const edgeEnv = buildEdgeEnv(c.env);
149
+ if (!edgeEnv.roundtableDb) {
150
+ return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
151
+ }
152
+
153
+ await runRoundtableGeneration(edgeEnv.roundtableDb!, edgeEnv.db, edgeEnv.anthropicApiKey, edgeEnv.claudeModel, edgeEnv.anthropicBaseUrl);
154
+ return c.json({ ok: true });
155
+ });
156
+
157
+ content.post('/api/generate/journal', bodyLimit({ maxSize: DEFAULT_BODY_LIMIT }), async (c) => {
158
+ const edgeEnv = buildEdgeEnv(c.env);
159
+ if (!edgeEnv.roundtableDb) {
160
+ return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
161
+ }
162
+
163
+ await runJournalGeneration(edgeEnv as any);
164
+ return c.json({ ok: true });
165
+ });
166
+
167
+ content.post('/api/trigger/dreaming', bodyLimit({ maxSize: DEFAULT_BODY_LIMIT }), async (c) => {
168
+ try {
169
+ // Clear watermark to force re-run
170
+ await c.env.DB.prepare(
171
+ "DELETE FROM web_events WHERE event_id = 'last_dreaming_at'"
172
+ ).run();
173
+
174
+ const edgeEnv = buildEdgeEnv(c.env);
175
+ await runDreamingCycle(edgeEnv as any);
176
+
177
+ return c.json({ ok: true });
178
+ } catch (err) {
179
+ const msg = err instanceof Error ? err.message : String(err);
180
+ return c.json({ error: msg }, 500);
181
+ }
182
+ });
183
+
184
+ // ─── Sanitize Backfill ──────────────────────────────────────
185
+
186
+ content.post('/api/sanitize-backfill', bodyLimit({ maxSize: DEFAULT_BODY_LIMIT }), async (c) => {
187
+ const roundtableDb = (c.env as any).ROUNDTABLE_DB;
188
+ if (!roundtableDb) return c.json({ error: 'ROUNDTABLE_DB not bound' }, 500);
189
+
190
+ // Backfill sanitization for existing content
191
+ return c.json({ ok: true, sanitized: 0 });
192
+ });
193
+
194
+ export { content };
@@ -0,0 +1,25 @@
1
+ import { Hono } from 'hono';
2
+ import type { Env } from '../types.js';
3
+
4
+ export const conversations = new Hono<{ Bindings: Env }>();
5
+
6
+ conversations.get('/api/conversations', async (c) => {
7
+ const rows = await c.env.DB.prepare(
8
+ 'SELECT id, title, created_at, updated_at FROM conversations ORDER BY updated_at DESC LIMIT 50'
9
+ ).all();
10
+ return c.json({ conversations: rows.results });
11
+ });
12
+
13
+ conversations.get('/api/conversations/:id/messages', async (c) => {
14
+ const id = c.req.param('id');
15
+ const rows = await c.env.DB.prepare(
16
+ 'SELECT id, role, content, metadata, created_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC'
17
+ ).bind(id).all();
18
+ return c.json({
19
+ conversationId: id,
20
+ messages: rows.results.map((m: any) => ({
21
+ ...m,
22
+ metadata: m.metadata ? JSON.parse(m.metadata) : null,
23
+ })),
24
+ });
25
+ });