@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.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- 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
|
+
});
|