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 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 /api/issues`
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
- - `/api/state` — runtime snapshot with capability counts
96
- - `/api/issues` — issue CRUD
97
- - `/api/events` — filtered event feed
98
- - `/api/issue/:id/pipeline` — pipeline snapshot for one issue
99
- - `/api/issue/:id/sessions` — session history for one issue
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/api/issues \
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/api/issues?state=Todo&capabilityCategory=devops'
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
- Endpoints:
281
-
282
- - `GET /api/issues?state=Todo&capabilityCategory=backend`
283
- - `POST /api/issues`
284
- - `PUT /api/issues/:id` — edit issue (title, description, priority, labels, paths, blockedBy)
285
- - `DELETE /api/issues/:id` — delete issue
286
- - `GET /api/events?issueId=LOCAL-1&kind=runner&since=2026-03-13T00:00:00.000Z`
287
- - `GET /api/issue/:id/pipeline`
288
- - `GET /api/issue/:id/sessions`
289
- - `POST /api/issue/:id/state`
290
- - `POST /api/issue/:id/retry`
291
- - `POST /api/issue/:id/cancel`
292
- - `GET /api/providers` — detected providers with availability status
293
- - `GET /api/parallelism` — parallelizability analysis for current issues
294
- - `POST /api/config/concurrency` — update worker concurrency at runtime
295
-
296
- The built-in dashboard now filters issues by both runtime state and capability category, and mirrors the scheduler's capability-aware ordering.
297
- `GET /api/state` and the MCP status summary also expose aggregated capability counts.
298
- The live events panel also filters by `kind` and `issueId`, backed by the partitioned `/api/events` route.
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
- - `symphifo_runtime_state`
303
- - `symphifo_issues`
304
- - `symphifo_events`
305
- - `symphifo_agent_sessions`
306
- - `symphifo_agent_pipelines`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symphifo",
3
- "version": "0.1.8",
3
+ "version": "0.1.9-next.b3eabea",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Filesystem-backed local Symphifo orchestrator with a TypeScript CLI, MCP mode, and multi-agent Codex or Claude workflows.",
@@ -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 /api/issues</p>
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(`/api/issue/${encodeURIComponent(issueId)}/sessions`);
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(`/api/issue/${encodeURIComponent(issueId)}/sessions`);
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("/api/issues", {
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(`/api/issue/${encodeURIComponent(issueId)}/state`, { state: currentState, reason: note });
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("/api/config/concurrency", { concurrency: num });
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("/api/providers");
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("/api/parallelism");
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(`/api/issue/${encodeURIComponent(issueId)}/state`, { state: nextState });
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(`/api/issue/${encodeURIComponent(issueId)}/retry`);
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(`/api/issue/${encodeURIComponent(issueId)}/cancel`);
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("/api/issues", payload);
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(`/api/issues/${encodeURIComponent(issueId)}`, {
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(`/api/issues/${encodeURIComponent(issueId)}`, { method: "DELETE" });
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(`/api/issue/${encodeURIComponent(id)}/retry`); } catch {}
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(`/api/issue/${encodeURIComponent(id)}/cancel`); } catch {}
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(`/api/events${query}`);
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("/api/state");
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
- const payload = await fetchJSON("/api/health");
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 = "symphifo_runtime_state";
93
- const ISSUE_RESOURCE = "symphifo_issues";
94
- const EVENT_RESOURCE = "symphifo_events";
95
- const SESSION_RESOURCE = "symphifo_agent_sessions";
96
- const PIPELINE_RESOURCE = "symphifo_agent_pipelines";
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", "/api/health"] },
146
+ logging: { enabled: true, excludePaths: ["/health", "/status"] },
129
147
  compression: { enabled: true, threshold: 1024 },
130
148
  health: { enabled: true },
131
149
  resources: {
132
- [S3DB_RUNTIME_RESOURCE]: { auth: false, methods: ["GET", "HEAD", "OPTIONS"] },
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 /api/state": async () => ({
153
+ "GET /state": async () => ({
140
154
  ...state,
141
155
  capabilities: computeCapabilityCounts(state.issues),
142
156
  }),
143
- "GET /api/health": async () => ({
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 /api/providers": async () => {
163
+ "GET /providers": async () => {
150
164
  const providers = detectAvailableProviders();
151
165
  return { providers };
152
166
  },
153
- "GET /api/parallelism": async () => {
167
+ "GET /parallelism": async () => {
154
168
  return analyzeParallelizability(state.issues);
155
169
  },
156
- "GET /api/issues": async (c: any) => {
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 /api/events": async (c: any) => {
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 /api/issue/:id/pipeline": async (c: any) => {
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 /api/issue/:id/sessions": async (c: any) => {
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 /api/issue/:id/state": async (c: any) => {
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 /api/issue/:id/retry": async (c: any) => {
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 /api/issue/:id/cancel": async (c: any) => {
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 /state": async (c: any) => c.redirect("/api/state"),
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}/api/state`);
264
+ logger.info(`State API: http://localhost:${port}/state`);
305
265
  logger.info(`OpenAPI docs available at http://localhost:${port}/docs`);
306
266
  }
@@ -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 = "symphifo_runtime_state";
72
- export const S3DB_ISSUE_RESOURCE = "symphifo_issues";
73
- export const S3DB_EVENT_RESOURCE = "symphifo_events";
74
- export const S3DB_AGENT_SESSION_RESOURCE = "symphifo_agent_sessions";
75
- export const S3DB_AGENT_PIPELINE_RESOURCE = "symphifo_agent_pipelines";
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