symphifo 0.1.8 → 0.1.9-next.b3eabea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -32
- package/package.json +1 -1
- package/src/dashboard/app.js +25 -19
- package/src/mcp/server.ts +5 -5
- package/src/runtime/api-server.ts +33 -73
- package/src/runtime/constants.ts +5 -5
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ npx -y symphifo --port 4040
|
|
|
72
72
|
Default local flow:
|
|
73
73
|
|
|
74
74
|
1. Open `http://localhost:4040`
|
|
75
|
-
2. Create an issue from the UI or `POST /
|
|
75
|
+
2. Create an issue from the UI or `POST /issues`
|
|
76
76
|
3. Add `labels` and `paths` when you want stronger automatic routing
|
|
77
77
|
4. Watch the queue, capability category, overlays, events, and agent sessions
|
|
78
78
|
5. Use `View Sessions` on an issue to inspect the current pipeline, turns, directives, and latest output
|
|
@@ -92,16 +92,17 @@ Useful app routes:
|
|
|
92
92
|
|
|
93
93
|
- `/` — dashboard
|
|
94
94
|
- `/docs` — OpenAPI docs from `ApiPlugin`
|
|
95
|
-
- `/
|
|
96
|
-
- `/
|
|
97
|
-
- `/
|
|
98
|
-
- `/
|
|
99
|
-
- `/
|
|
95
|
+
- `/state` — runtime snapshot with capability counts
|
|
96
|
+
- `/issues` — issue CRUD (GET, POST, PUT, DELETE)
|
|
97
|
+
- `/events` — event records
|
|
98
|
+
- `/events/feed` — filtered event feed with `?since=&kind=&issueId=`
|
|
99
|
+
- `/issue/:id/pipeline` — pipeline snapshot for one issue
|
|
100
|
+
- `/issue/:id/sessions` — session history for one issue
|
|
100
101
|
|
|
101
102
|
Useful API examples:
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
|
-
curl -X POST http://localhost:4040/
|
|
105
|
+
curl -X POST http://localhost:4040/issues \
|
|
105
106
|
-H 'content-type: application/json' \
|
|
106
107
|
-d '{
|
|
107
108
|
"title":"Prepare release notes",
|
|
@@ -111,7 +112,7 @@ curl -X POST http://localhost:4040/api/issues \
|
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
```bash
|
|
114
|
-
curl 'http://localhost:4040/
|
|
115
|
+
curl 'http://localhost:4040/issues?state=Todo&capabilityCategory=devops'
|
|
115
116
|
```
|
|
116
117
|
|
|
117
118
|
## Package layout
|
|
@@ -277,33 +278,42 @@ The scheduler uses capability priority as a tie-breaker after issue priority, an
|
|
|
277
278
|
|
|
278
279
|
## HTTP surface
|
|
279
280
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
- `GET /
|
|
283
|
-
- `POST /
|
|
284
|
-
- `PUT /
|
|
285
|
-
- `DELETE /
|
|
286
|
-
- `GET /
|
|
287
|
-
- `GET /
|
|
288
|
-
- `GET /
|
|
289
|
-
- `
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
- `GET /
|
|
294
|
-
- `
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
`GET /
|
|
298
|
-
|
|
281
|
+
Resource endpoints (s3db auto-generated):
|
|
282
|
+
|
|
283
|
+
- `GET /issues` — list issues (supports `?state=&capabilityCategory=`)
|
|
284
|
+
- `POST /issues` — create issue
|
|
285
|
+
- `PUT /issues/:id` — update issue
|
|
286
|
+
- `DELETE /issues/:id` — delete issue
|
|
287
|
+
- `GET /events` — list events
|
|
288
|
+
- `GET /runtime_state` — raw runtime state
|
|
289
|
+
- `GET /agent_sessions` — list agent sessions
|
|
290
|
+
- `GET /agent_pipelines` — list agent pipelines
|
|
291
|
+
|
|
292
|
+
Custom endpoints:
|
|
293
|
+
|
|
294
|
+
- `GET /state` — runtime snapshot with capability counts
|
|
295
|
+
- `GET /status` — health check
|
|
296
|
+
- `GET /events/feed?since=&kind=&issueId=` — filtered event feed
|
|
297
|
+
- `GET /issue/:id/pipeline` — pipeline snapshot for one issue
|
|
298
|
+
- `GET /issue/:id/sessions` — session history for one issue
|
|
299
|
+
- `POST /issue/:id/state` — transition issue state
|
|
300
|
+
- `POST /issue/:id/retry` — retry issue
|
|
301
|
+
- `POST /issue/:id/cancel` — cancel issue
|
|
302
|
+
- `GET /providers` — detected providers with availability
|
|
303
|
+
- `GET /parallelism` — parallelizability analysis
|
|
304
|
+
- `POST /config/concurrency` — update worker concurrency
|
|
305
|
+
|
|
306
|
+
The built-in dashboard filters issues by both runtime state and capability category, and mirrors the scheduler's capability-aware ordering.
|
|
307
|
+
`GET /state` and the MCP status summary also expose aggregated capability counts.
|
|
308
|
+
The live events panel filters by `kind` and `issueId`, backed by the partitioned `/events/feed` route.
|
|
299
309
|
|
|
300
310
|
Native `ApiPlugin` resources:
|
|
301
311
|
|
|
302
|
-
- `
|
|
303
|
-
- `
|
|
304
|
-
- `
|
|
305
|
-
- `
|
|
306
|
-
- `
|
|
312
|
+
- `runtime_state`
|
|
313
|
+
- `issues`
|
|
314
|
+
- `events`
|
|
315
|
+
- `agent_sessions`
|
|
316
|
+
- `agent_pipelines`
|
|
307
317
|
|
|
308
318
|
These resources also define `s3db` partitions for the main operational access patterns:
|
|
309
319
|
|
package/package.json
CHANGED
package/src/dashboard/app.js
CHANGED
|
@@ -244,7 +244,7 @@ function renderOverview(metrics, issues = []) {
|
|
|
244
244
|
<div class="kpi" style="grid-column: 1 / -1; padding: 24px;">
|
|
245
245
|
<p class="label">No Issues</p>
|
|
246
246
|
<p class="value" style="font-size: 1.2rem;">Create your first issue to get started</p>
|
|
247
|
-
<p class="desc">Use the "+ New" button above or POST to /
|
|
247
|
+
<p class="desc">Use the "+ New" button above or POST to /issues</p>
|
|
248
248
|
</div>
|
|
249
249
|
`;
|
|
250
250
|
const existing = document.getElementById("progress-bar");
|
|
@@ -595,7 +595,7 @@ async function loadSessionsForPanel(issueId, panelElementId) {
|
|
|
595
595
|
if (!panel) return;
|
|
596
596
|
|
|
597
597
|
try {
|
|
598
|
-
const data = await fetchJSON(`/
|
|
598
|
+
const data = await fetchJSON(`/issue/${encodeURIComponent(issueId)}/sessions`);
|
|
599
599
|
let html = "";
|
|
600
600
|
|
|
601
601
|
if (data.pipeline) {
|
|
@@ -662,7 +662,7 @@ async function loadSessionsForIssue(issueId) {
|
|
|
662
662
|
if (!panel) return;
|
|
663
663
|
|
|
664
664
|
try {
|
|
665
|
-
const data = await fetchJSON(`/
|
|
665
|
+
const data = await fetchJSON(`/issue/${encodeURIComponent(issueId)}/sessions`);
|
|
666
666
|
let html = "";
|
|
667
667
|
|
|
668
668
|
if (data.pipeline) {
|
|
@@ -762,7 +762,7 @@ async function submitSplit(issueId, target) {
|
|
|
762
762
|
try {
|
|
763
763
|
const created = [];
|
|
764
764
|
for (const title of titles) {
|
|
765
|
-
const result = await post("/
|
|
765
|
+
const result = await post("/issues", {
|
|
766
766
|
title,
|
|
767
767
|
description: `Sub-task of ${issue.identifier}: ${issue.title}`,
|
|
768
768
|
priority: issue.priority,
|
|
@@ -793,7 +793,7 @@ async function addNote(issueId, target) {
|
|
|
793
793
|
|
|
794
794
|
await withLoading(target, async () => {
|
|
795
795
|
try {
|
|
796
|
-
await post(`/
|
|
796
|
+
await post(`/issue/${encodeURIComponent(issueId)}/state`, { state: currentState, reason: note });
|
|
797
797
|
input.value = "";
|
|
798
798
|
showToast("Note added", "success", 2000);
|
|
799
799
|
await loadState();
|
|
@@ -829,7 +829,7 @@ function renderRuntimeMeta(state) {
|
|
|
829
829
|
if (!num || num < 1 || num > 16) { showToast("Must be 1-16", "warn"); return; }
|
|
830
830
|
await withLoading(e.target, async () => {
|
|
831
831
|
try {
|
|
832
|
-
await post("/
|
|
832
|
+
await post("/config/concurrency", { concurrency: num });
|
|
833
833
|
showToast(`Concurrency set to ${num}`, "success");
|
|
834
834
|
await loadState();
|
|
835
835
|
} catch (err) { showToast(err.message); }
|
|
@@ -844,7 +844,7 @@ async function loadProviders() {
|
|
|
844
844
|
const panel = document.getElementById("providers-panel");
|
|
845
845
|
if (!panel) return;
|
|
846
846
|
try {
|
|
847
|
-
const data = await fetchJSON("/
|
|
847
|
+
const data = await fetchJSON("/providers");
|
|
848
848
|
if (!data.providers || !data.providers.length) { panel.innerHTML = ""; return; }
|
|
849
849
|
panel.innerHTML = "Providers: " + data.providers.map((p) =>
|
|
850
850
|
`<span class="tag ${p.available ? "tag-ok" : "tag-missing"}">${escapeHtml(p.name)}: ${p.available ? "available" : "not found"}</span>`
|
|
@@ -856,7 +856,7 @@ async function loadParallelism() {
|
|
|
856
856
|
const panel = document.getElementById("parallelism-panel");
|
|
857
857
|
if (!panel) return;
|
|
858
858
|
try {
|
|
859
|
-
const data = await fetchJSON("/
|
|
859
|
+
const data = await fetchJSON("/parallelism");
|
|
860
860
|
if (!data.reason) { panel.innerHTML = ""; return; }
|
|
861
861
|
const badge = data.canParallelize ? "tag-ok" : "tag-missing";
|
|
862
862
|
panel.innerHTML = `Parallelism: <span class="tag ${badge}">max safe=${data.maxSafeParallelism}</span> <span class="muted">${escapeHtml(data.reason)}</span>`;
|
|
@@ -916,21 +916,21 @@ function renderEvents(events = []) {
|
|
|
916
916
|
|
|
917
917
|
async function setIssueState(issueId, nextState, target) {
|
|
918
918
|
await withLoading(target, async () => {
|
|
919
|
-
await post(`/
|
|
919
|
+
await post(`/issue/${encodeURIComponent(issueId)}/state`, { state: nextState });
|
|
920
920
|
await loadState();
|
|
921
921
|
});
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
async function retryIssue(issueId, target) {
|
|
925
925
|
await withLoading(target, async () => {
|
|
926
|
-
await post(`/
|
|
926
|
+
await post(`/issue/${encodeURIComponent(issueId)}/retry`);
|
|
927
927
|
await loadState();
|
|
928
928
|
});
|
|
929
929
|
}
|
|
930
930
|
|
|
931
931
|
async function cancelIssue(issueId, target) {
|
|
932
932
|
await withLoading(target, async () => {
|
|
933
|
-
await post(`/
|
|
933
|
+
await post(`/issue/${encodeURIComponent(issueId)}/cancel`);
|
|
934
934
|
await loadState();
|
|
935
935
|
});
|
|
936
936
|
}
|
|
@@ -957,7 +957,7 @@ async function submitCreateForm(target) {
|
|
|
957
957
|
|
|
958
958
|
await withLoading(target, async () => {
|
|
959
959
|
try {
|
|
960
|
-
const result = await post("/
|
|
960
|
+
const result = await post("/issues", payload);
|
|
961
961
|
showToast(`Created ${result.issue?.identifier || "issue"}`, "success");
|
|
962
962
|
createForm.hidden = true;
|
|
963
963
|
document.getElementById("cf-title").value = "";
|
|
@@ -1033,7 +1033,7 @@ async function submitEdit(issueId, target) {
|
|
|
1033
1033
|
|
|
1034
1034
|
await withLoading(target, async () => {
|
|
1035
1035
|
try {
|
|
1036
|
-
const response = await fetch(`/
|
|
1036
|
+
const response = await fetch(`/issues/${encodeURIComponent(issueId)}`, {
|
|
1037
1037
|
method: "PUT",
|
|
1038
1038
|
headers: { "content-type": "application/json" },
|
|
1039
1039
|
body: JSON.stringify({
|
|
@@ -1063,7 +1063,7 @@ function requestDelete(issueId) {
|
|
|
1063
1063
|
async function confirmDelete(issueId, target) {
|
|
1064
1064
|
await withLoading(target, async () => {
|
|
1065
1065
|
try {
|
|
1066
|
-
const response = await fetch(`/
|
|
1066
|
+
const response = await fetch(`/issues/${encodeURIComponent(issueId)}`, { method: "DELETE" });
|
|
1067
1067
|
if (!response.ok) throw new Error(`Failed: ${response.status}`);
|
|
1068
1068
|
const issue = (appState.issues || []).find((i) => i.id === issueId);
|
|
1069
1069
|
pendingDeleteId = null;
|
|
@@ -1159,7 +1159,7 @@ function wireActions() {
|
|
|
1159
1159
|
if (target.id === "batch-retry") {
|
|
1160
1160
|
const ids = [...selectedIssues];
|
|
1161
1161
|
for (const id of ids) {
|
|
1162
|
-
try { await post(`/
|
|
1162
|
+
try { await post(`/issue/${encodeURIComponent(id)}/retry`); } catch {}
|
|
1163
1163
|
}
|
|
1164
1164
|
selectedIssues.clear();
|
|
1165
1165
|
showToast(`Retried ${ids.length} issues`, "success");
|
|
@@ -1167,7 +1167,7 @@ function wireActions() {
|
|
|
1167
1167
|
} else if (target.id === "batch-cancel") {
|
|
1168
1168
|
const ids = [...selectedIssues];
|
|
1169
1169
|
for (const id of ids) {
|
|
1170
|
-
try { await post(`/
|
|
1170
|
+
try { await post(`/issue/${encodeURIComponent(id)}/cancel`); } catch {}
|
|
1171
1171
|
}
|
|
1172
1172
|
selectedIssues.clear();
|
|
1173
1173
|
showToast(`Cancelled ${ids.length} issues`, "success");
|
|
@@ -1208,7 +1208,7 @@ async function loadEvents() {
|
|
|
1208
1208
|
if (eventKindFilter?.value && eventKindFilter.value !== "all") params.set("kind", eventKindFilter.value);
|
|
1209
1209
|
if (eventIssueFilter?.value && eventIssueFilter.value !== "all") params.set("issueId", eventIssueFilter.value);
|
|
1210
1210
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1211
|
-
const payload = await fetchJSON(`/
|
|
1211
|
+
const payload = await fetchJSON(`/events/feed${query}`);
|
|
1212
1212
|
const events = Array.isArray(payload.events) ? payload.events : [];
|
|
1213
1213
|
|
|
1214
1214
|
if (events.length > 0) {
|
|
@@ -1223,7 +1223,7 @@ async function loadEvents() {
|
|
|
1223
1223
|
}
|
|
1224
1224
|
|
|
1225
1225
|
async function loadState() {
|
|
1226
|
-
const payload = await fetchJSON("/
|
|
1226
|
+
const payload = await fetchJSON("/state");
|
|
1227
1227
|
|
|
1228
1228
|
// Diff: skip re-render if nothing changed
|
|
1229
1229
|
const hash = simpleHash(JSON.stringify(payload.issues) + JSON.stringify(payload.metrics));
|
|
@@ -1260,7 +1260,13 @@ async function loadState() {
|
|
|
1260
1260
|
|
|
1261
1261
|
async function loadHealth() {
|
|
1262
1262
|
try {
|
|
1263
|
-
|
|
1263
|
+
let payload = null;
|
|
1264
|
+
try {
|
|
1265
|
+
payload = await fetchJSON("/status");
|
|
1266
|
+
} catch {
|
|
1267
|
+
payload = await fetchJSON("/health");
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1264
1270
|
const status = payload.status || "ok";
|
|
1265
1271
|
healthBadge.textContent = `status: ${status}`;
|
|
1266
1272
|
healthBadge.className = `badge badge-health-${status === "ok" ? "ok" : "warn"}`;
|
package/src/mcp/server.ts
CHANGED
|
@@ -89,11 +89,11 @@ const STORAGE_BUCKET = env.SYMPHIFO_STORAGE_BUCKET ?? "symphifo";
|
|
|
89
89
|
const STORAGE_KEY_PREFIX = env.SYMPHIFO_STORAGE_KEY_PREFIX ?? "state";
|
|
90
90
|
const STORAGE_LIBRARY_PATH = "";
|
|
91
91
|
const DEBUG_BOOT = env.SYMPHIFO_DEBUG_BOOT === "1";
|
|
92
|
-
const RUNTIME_RESOURCE = "
|
|
93
|
-
const ISSUE_RESOURCE = "
|
|
94
|
-
const EVENT_RESOURCE = "
|
|
95
|
-
const SESSION_RESOURCE = "
|
|
96
|
-
const PIPELINE_RESOURCE = "
|
|
92
|
+
const RUNTIME_RESOURCE = "runtime_state";
|
|
93
|
+
const ISSUE_RESOURCE = "issues";
|
|
94
|
+
const EVENT_RESOURCE = "events";
|
|
95
|
+
const SESSION_RESOURCE = "agent_sessions";
|
|
96
|
+
const PIPELINE_RESOURCE = "agent_pipelines";
|
|
97
97
|
const RUNTIME_RECORD_ID = "current";
|
|
98
98
|
|
|
99
99
|
let incomingBuffer = Buffer.alloc(0);
|
|
@@ -119,93 +119,55 @@ export async function startApiServer(
|
|
|
119
119
|
: events;
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
+
const resourceConfigs: Record<string, Record<string, unknown>> = {
|
|
123
|
+
[S3DB_RUNTIME_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
124
|
+
[S3DB_ISSUE_RESOURCE]: { auth: false, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] },
|
|
125
|
+
[S3DB_EVENT_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
126
|
+
[S3DB_AGENT_SESSION_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
127
|
+
[S3DB_AGENT_PIPELINE_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const existingResources = await (stateDb as { listResources?: () => Promise<Array<{ name: string }>> }).listResources?.();
|
|
131
|
+
for (const item of existingResources || []) {
|
|
132
|
+
if (typeof item?.name === "string" && item.name.startsWith("symphifo_") && !resourceConfigs[item.name]) {
|
|
133
|
+
resourceConfigs[item.name] = { enabled: false };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dashboardHtml = indexHtml || fallback;
|
|
138
|
+
|
|
122
139
|
const apiPlugin = new ApiPlugin({
|
|
123
140
|
port,
|
|
124
141
|
host: "0.0.0.0",
|
|
125
142
|
versionPrefix: false,
|
|
143
|
+
rootRoute: (c: any) => c.html(dashboardHtml),
|
|
126
144
|
docs: { enabled: true, title: "Symphifo API", version: "1.0.0", description: "Local orchestration API for Symphifo" },
|
|
127
145
|
cors: { enabled: true, origin: "*" },
|
|
128
|
-
logging: { enabled: true, excludePaths: ["/health", "/
|
|
146
|
+
logging: { enabled: true, excludePaths: ["/health", "/status"] },
|
|
129
147
|
compression: { enabled: true, threshold: 1024 },
|
|
130
148
|
health: { enabled: true },
|
|
131
149
|
resources: {
|
|
132
|
-
|
|
133
|
-
[S3DB_ISSUE_RESOURCE]: { auth: false, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] },
|
|
134
|
-
[S3DB_EVENT_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
135
|
-
[S3DB_AGENT_SESSION_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
136
|
-
[S3DB_AGENT_PIPELINE_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
|
|
150
|
+
...resourceConfigs,
|
|
137
151
|
},
|
|
138
152
|
routes: {
|
|
139
|
-
"GET /
|
|
153
|
+
"GET /state": async () => ({
|
|
140
154
|
...state,
|
|
141
155
|
capabilities: computeCapabilityCounts(state.issues),
|
|
142
156
|
}),
|
|
143
|
-
"GET /
|
|
157
|
+
"GET /status": async () => ({
|
|
144
158
|
status: "ok",
|
|
145
159
|
updatedAt: state.updatedAt,
|
|
146
160
|
config: state.config,
|
|
147
161
|
trackerKind: state.trackerKind,
|
|
148
162
|
}),
|
|
149
|
-
"GET /
|
|
163
|
+
"GET /providers": async () => {
|
|
150
164
|
const providers = detectAvailableProviders();
|
|
151
165
|
return { providers };
|
|
152
166
|
},
|
|
153
|
-
"GET /
|
|
167
|
+
"GET /parallelism": async () => {
|
|
154
168
|
return analyzeParallelizability(state.issues);
|
|
155
169
|
},
|
|
156
|
-
"
|
|
157
|
-
const issueState = c.req.query("state");
|
|
158
|
-
const capabilityCategory = c.req.query("capabilityCategory") ?? c.req.query("category");
|
|
159
|
-
const issues = await listIssues({
|
|
160
|
-
state: typeof issueState === "string" && issueState ? issueState : undefined,
|
|
161
|
-
capabilityCategory: typeof capabilityCategory === "string" && capabilityCategory ? capabilityCategory : undefined,
|
|
162
|
-
});
|
|
163
|
-
return { issues };
|
|
164
|
-
},
|
|
165
|
-
"POST /api/issues": async (c: any) => {
|
|
166
|
-
const payload = await c.req.json() as JsonRecord;
|
|
167
|
-
const issue = createIssueFromPayload(payload, state.issues, workflowDefinition);
|
|
168
|
-
const duplicate = state.issues.find((candidate) => candidate.id === issue.id || candidate.identifier === issue.identifier);
|
|
169
|
-
|
|
170
|
-
if (duplicate) {
|
|
171
|
-
return c.json({ ok: false, error: "Issue id or identifier already exists", issue: duplicate }, 409);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
state.issues.push(issue);
|
|
175
|
-
state.updatedAt = now();
|
|
176
|
-
addEvent(state, issue.id, "manual", `Issue ${issue.identifier} created via API.`);
|
|
177
|
-
await persistState(state);
|
|
178
|
-
return c.json({ ok: true, issue }, 201);
|
|
179
|
-
},
|
|
180
|
-
"PUT /api/issues/:id": async (c: any) => {
|
|
181
|
-
const issue = findIssue(c.req.param("id"));
|
|
182
|
-
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
183
|
-
|
|
184
|
-
const payload = await c.req.json() as JsonRecord;
|
|
185
|
-
if (typeof payload.title === "string") issue.title = payload.title;
|
|
186
|
-
if (typeof payload.description === "string") issue.description = payload.description;
|
|
187
|
-
if (typeof payload.priority === "number") issue.priority = clamp(payload.priority, 1, 10);
|
|
188
|
-
if (Array.isArray(payload.labels)) issue.labels = payload.labels.filter((l: unknown): l is string => typeof l === "string");
|
|
189
|
-
if (Array.isArray(payload.paths)) issue.paths = payload.paths.filter((p: unknown): p is string => typeof p === "string");
|
|
190
|
-
if (Array.isArray(payload.blockedBy)) issue.blockedBy = payload.blockedBy.filter((b: unknown): b is string => typeof b === "string");
|
|
191
|
-
|
|
192
|
-
issue.updatedAt = now();
|
|
193
|
-
addEvent(state, issue.id, "manual", `Issue ${issue.identifier} updated via API.`);
|
|
194
|
-
await persistState(state);
|
|
195
|
-
return { ok: true, issue };
|
|
196
|
-
},
|
|
197
|
-
"DELETE /api/issues/:id": async (c: any) => {
|
|
198
|
-
const issueId = c.req.param("id");
|
|
199
|
-
const index = state.issues.findIndex((i) => i.id === issueId || i.identifier === issueId);
|
|
200
|
-
if (index === -1) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
201
|
-
|
|
202
|
-
const removed = state.issues.splice(index, 1)[0];
|
|
203
|
-
state.updatedAt = now();
|
|
204
|
-
addEvent(state, removed.id, "manual", `Issue ${removed.identifier} deleted via API.`);
|
|
205
|
-
await persistState(state);
|
|
206
|
-
return { ok: true, deleted: removed.identifier };
|
|
207
|
-
},
|
|
208
|
-
"POST /api/config/concurrency": async (c: any) => {
|
|
170
|
+
"POST /config/concurrency": async (c: any) => {
|
|
209
171
|
const payload = await c.req.json() as JsonRecord;
|
|
210
172
|
const value = typeof payload.concurrency === "number" ? payload.concurrency : undefined;
|
|
211
173
|
if (!value || value < 1 || value > 16) {
|
|
@@ -217,7 +179,7 @@ export async function startApiServer(
|
|
|
217
179
|
await persistState(state);
|
|
218
180
|
return { ok: true, workerConcurrency: state.config.workerConcurrency };
|
|
219
181
|
},
|
|
220
|
-
"GET /
|
|
182
|
+
"GET /events/feed": async (c: any) => {
|
|
221
183
|
const since = c.req.query("since");
|
|
222
184
|
const issueId = c.req.query("issueId");
|
|
223
185
|
const kind = c.req.query("kind");
|
|
@@ -228,7 +190,7 @@ export async function startApiServer(
|
|
|
228
190
|
});
|
|
229
191
|
return { events: events.slice(0, 200) };
|
|
230
192
|
},
|
|
231
|
-
"GET /
|
|
193
|
+
"GET /issue/:id/pipeline": async (c: any) => {
|
|
232
194
|
const issue = findIssue(c.req.param("id"));
|
|
233
195
|
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
234
196
|
|
|
@@ -236,7 +198,7 @@ export async function startApiServer(
|
|
|
236
198
|
const pipeline = await loadAgentPipelineSnapshotForIssue(issue, providers);
|
|
237
199
|
return { ok: true, issueId: issue.id, pipeline };
|
|
238
200
|
},
|
|
239
|
-
"GET /
|
|
201
|
+
"GET /issue/:id/sessions": async (c: any) => {
|
|
240
202
|
const issue = findIssue(c.req.param("id"));
|
|
241
203
|
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
242
204
|
|
|
@@ -245,7 +207,7 @@ export async function startApiServer(
|
|
|
245
207
|
const sessions = await loadAgentSessionSnapshotsForIssue(issue, providers, pipeline, workflowDefinition);
|
|
246
208
|
return { ok: true, issueId: issue.id, pipeline, sessions };
|
|
247
209
|
},
|
|
248
|
-
"POST /
|
|
210
|
+
"POST /issue/:id/state": async (c: any) => {
|
|
249
211
|
const issue = findIssue(c.req.param("id"));
|
|
250
212
|
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
251
213
|
|
|
@@ -258,7 +220,7 @@ export async function startApiServer(
|
|
|
258
220
|
return c.json({ ok: false, error: String(error) }, 400);
|
|
259
221
|
}
|
|
260
222
|
},
|
|
261
|
-
"POST /
|
|
223
|
+
"POST /issue/:id/retry": async (c: any) => {
|
|
262
224
|
const issue = findIssue(c.req.param("id"));
|
|
263
225
|
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
264
226
|
|
|
@@ -277,7 +239,7 @@ export async function startApiServer(
|
|
|
277
239
|
await persistState(state);
|
|
278
240
|
return { ok: true, issue };
|
|
279
241
|
},
|
|
280
|
-
"POST /
|
|
242
|
+
"POST /issue/:id/cancel": async (c: any) => {
|
|
281
243
|
const issue = findIssue(c.req.param("id"));
|
|
282
244
|
if (!issue) return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
283
245
|
|
|
@@ -286,9 +248,7 @@ export async function startApiServer(
|
|
|
286
248
|
await persistState(state);
|
|
287
249
|
return { ok: true, issue };
|
|
288
250
|
},
|
|
289
|
-
"GET /
|
|
290
|
-
"GET /": async (c: any) => c.html(indexHtml || fallback),
|
|
291
|
-
"GET /index.html": async (c: any) => c.html(indexHtml || fallback),
|
|
251
|
+
"GET /index.html": async (c: any) => c.html(dashboardHtml),
|
|
292
252
|
"GET /assets/app.js": async (c: any) => c.body(appJs || "console.log('Dashboard script not found.');", 200, {
|
|
293
253
|
"content-type": "application/javascript; charset=utf-8",
|
|
294
254
|
}),
|
|
@@ -301,6 +261,6 @@ export async function startApiServer(
|
|
|
301
261
|
const plugin = await stateDb.usePlugin(apiPlugin, "api") as { stop?: () => Promise<void> };
|
|
302
262
|
setActiveApiPlugin(plugin);
|
|
303
263
|
logger.info(`Local dashboard available at http://localhost:${port}`);
|
|
304
|
-
logger.info(`State API: http://localhost:${port}/
|
|
264
|
+
logger.info(`State API: http://localhost:${port}/state`);
|
|
305
265
|
logger.info(`OpenAPI docs available at http://localhost:${port}/docs`);
|
|
306
266
|
}
|
package/src/runtime/constants.ts
CHANGED
|
@@ -68,11 +68,11 @@ export const S3DB_DATABASE_PATH = `${STATE_ROOT}/s3db`;
|
|
|
68
68
|
export const S3DB_BUCKET = env.SYMPHIFO_STORAGE_BUCKET ?? "symphifo";
|
|
69
69
|
export const S3DB_KEY_PREFIX = env.SYMPHIFO_STORAGE_KEY_PREFIX ?? "state";
|
|
70
70
|
|
|
71
|
-
export const S3DB_RUNTIME_RESOURCE = "
|
|
72
|
-
export const S3DB_ISSUE_RESOURCE = "
|
|
73
|
-
export const S3DB_EVENT_RESOURCE = "
|
|
74
|
-
export const S3DB_AGENT_SESSION_RESOURCE = "
|
|
75
|
-
export const S3DB_AGENT_PIPELINE_RESOURCE = "
|
|
71
|
+
export const S3DB_RUNTIME_RESOURCE = "runtime_state";
|
|
72
|
+
export const S3DB_ISSUE_RESOURCE = "issues";
|
|
73
|
+
export const S3DB_EVENT_RESOURCE = "events";
|
|
74
|
+
export const S3DB_AGENT_SESSION_RESOURCE = "agent_sessions";
|
|
75
|
+
export const S3DB_AGENT_PIPELINE_RESOURCE = "agent_pipelines";
|
|
76
76
|
export const S3DB_RUNTIME_RECORD_ID = "current";
|
|
77
77
|
export const S3DB_RUNTIME_SCHEMA_VERSION = 1;
|
|
78
78
|
|