Initial import: open-design source for helix-mind.ai distribution
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
This repository contains the open-design daemon CLI source code, built and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use by the HelixMind /design slash command. Licenses: Apache-2.0 (root) + MIT (skills/*)
This commit is contained in:
346
specs/2026-04-29-live-artifacts/checklist.md
Normal file
346
specs/2026-04-29-live-artifacts/checklist.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Live Artifacts Implementation Checklist
|
||||
|
||||
## Phase 0 — Contracts and fixtures
|
||||
|
||||
- [ ] Define `html_template_v1` rendering contract.
|
||||
- [ ] Decide MVP binding syntax: Mustache-style `{{data.path}}`, `data-od-*` attributes, or both.
|
||||
- [ ] Specify that interpolation HTML-escapes by default.
|
||||
- [ ] Explicitly disallow raw / unescaped interpolation in MVP.
|
||||
- [ ] Specify allowed structural directives, if any.
|
||||
- [ ] Define shared bounded JSON constraints.
|
||||
- [ ] Max object depth.
|
||||
- [ ] Max array length.
|
||||
- [ ] Max string length.
|
||||
- [ ] Max serialized payload size.
|
||||
- [ ] Define shared contract DTOs for core types.
|
||||
- [ ] Add shared DTOs under `packages/contracts/src/api/live-artifacts.ts`.
|
||||
- [ ] Add connector DTOs under `packages/contracts/src/api/connectors.ts`.
|
||||
- [ ] Export new contract modules from `packages/contracts/src/index.ts`.
|
||||
- [ ] `BoundedJsonValue` / `BoundedJsonObject`.
|
||||
- [ ] `LiveArtifact`.
|
||||
- [ ] `LiveArtifactDocument`.
|
||||
- [ ] `LiveArtifactSource`.
|
||||
- [ ] `LiveArtifactProvenance`.
|
||||
- [ ] `ConnectorDetail`.
|
||||
- [ ] `ConnectorToolSafety`.
|
||||
- [ ] Implement daemon runtime validation schemas in `apps/daemon/src/live-artifacts/schema.ts`.
|
||||
- [ ] Consume or mirror shared contract DTOs without adding daemon internals to `packages/contracts`.
|
||||
- [ ] Validate persisted artifact files and create/update inputs.
|
||||
- [ ] Define create/update input schemas.
|
||||
- [ ] `LiveArtifactCreateInput`.
|
||||
- [ ] `LiveArtifactUpdateInput`.
|
||||
- [ ] Reject daemon-owned fields from agent input: `id`, `projectId`, `createdAt`, `createdByRunId`, `schemaVersion`, `refreshStatus`.
|
||||
- [ ] Extend the shared API error envelope for live artifact and connector failures.
|
||||
- [ ] Add live artifact / connector `ApiErrorCode` values in `packages/contracts/src/errors.ts`.
|
||||
- [ ] Use `ApiErrorResponse` for agent-facing tool endpoint failures.
|
||||
- [ ] Put validation field details in the existing error `details` field.
|
||||
- [ ] Add fixture artifacts under `specs/2026-04-29-live-artifacts/examples/`.
|
||||
- [ ] Minimal static `html_template_v1` artifact.
|
||||
- [ ] Invalid fixture containing forbidden raw provider fields.
|
||||
- [ ] Invalid fixture containing credential-like fields.
|
||||
- [ ] Add schema validation tests.
|
||||
- [ ] Reject `raw`, `rawResponse`, `payload`, `body`, `headers`, `cookie`, `authorization`, `token`, `secret`, `credential`, `password` keys.
|
||||
- [ ] Reject path traversal.
|
||||
- [ ] Reject oversized JSON.
|
||||
- [ ] Accept valid fixture artifacts.
|
||||
|
||||
## Phase 1A — Static live artifact registration
|
||||
|
||||
- [ ] Create daemon live artifact module structure.
|
||||
- [ ] `apps/daemon/src/live-artifacts/schema.ts`.
|
||||
- [ ] `apps/daemon/src/live-artifacts/store.ts`.
|
||||
- [ ] `apps/daemon/src/live-artifacts/render.ts`.
|
||||
- [ ] Implement project-scoped storage layout.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/artifact.json`.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/template.html`.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/data.json`.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/provenance.json`.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/refreshes.jsonl`.
|
||||
- [ ] `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts/<artifactId>/snapshots/`.
|
||||
- [ ] Support `OD_DATA_DIR` override through existing daemon runtime data-dir conventions.
|
||||
- [ ] Implement artifact ID and slug generation.
|
||||
- [ ] Implement safe path resolution.
|
||||
- [ ] Ensure all reads/writes stay inside project workspace.
|
||||
- [ ] Reject absolute paths from agent payloads.
|
||||
- [ ] Reject `..` traversal.
|
||||
- [ ] Implement `createLiveArtifact` service.
|
||||
- [ ] Validate create input.
|
||||
- [ ] Assign daemon-owned fields.
|
||||
- [ ] Persist files atomically where practical.
|
||||
- [ ] Generate initial `index.html` as derived preview output.
|
||||
- [ ] Implement `listLiveArtifacts` service.
|
||||
- [ ] Read project live artifact directories.
|
||||
- [ ] Return compact summaries.
|
||||
- [ ] Hide snapshots and implementation files.
|
||||
- [ ] Implement `getLiveArtifact` service.
|
||||
- [ ] Add daemon routes for UI reads.
|
||||
- [ ] `GET /api/live-artifacts?projectId=...`.
|
||||
- [ ] `GET /api/live-artifacts/:artifactId`.
|
||||
- [ ] Add daemon tool routes for agent registration.
|
||||
- [ ] `POST /api/tools/live-artifacts/create`.
|
||||
- [ ] `GET /api/tools/live-artifacts/list`.
|
||||
- [ ] Implement run-scoped tool token infrastructure.
|
||||
- [ ] Define run as one `/api/chat` invocation.
|
||||
- [ ] Mint `OD_TOOL_TOKEN` per run.
|
||||
- [ ] Bind token to `runId`, `projectId`, allowed endpoints, allowed operations, and expiry.
|
||||
- [ ] Revoke token when child process exits.
|
||||
- [ ] Revoke token when SSE stream ends.
|
||||
- [ ] Revoke token when TTL expires.
|
||||
- [ ] Isolate concurrent runs under the same project.
|
||||
- [ ] Inject runtime tool environment into agent session.
|
||||
- [ ] `OD_DAEMON_URL`.
|
||||
- [ ] `OD_TOOL_TOKEN`.
|
||||
- [ ] Enforce `/api/tools/*` auth.
|
||||
- [ ] Require bearer token.
|
||||
- [ ] Derive project scope from token.
|
||||
- [ ] Reject request-supplied project overrides.
|
||||
- [ ] Enforce endpoint allowlist.
|
||||
- [ ] Enforce operation allowlist.
|
||||
|
||||
## Phase 1B — Preview and UI integration
|
||||
|
||||
- [ ] Implement preview renderer.
|
||||
- [ ] Load `template.html`.
|
||||
- [ ] Load `data.json` as source of truth.
|
||||
- [ ] Apply `html_template_v1` bindings.
|
||||
- [ ] HTML-escape interpolated values.
|
||||
- [ ] Regenerate derived `index.html` when needed.
|
||||
- [ ] Add preview route.
|
||||
- [ ] `GET /api/live-artifacts/:artifactId/preview`.
|
||||
- [ ] Serve with iframe-compatible response.
|
||||
- [ ] Apply restrictive CSP.
|
||||
- [ ] Apply iframe sandbox assumptions.
|
||||
- [ ] Apply local daemon security to preview routes.
|
||||
- [ ] Host header validation.
|
||||
- [ ] Origin validation.
|
||||
- [ ] CORS restrictions.
|
||||
- [ ] CSRF protection where relevant.
|
||||
- [ ] DNS rebinding protection.
|
||||
- [ ] Add live artifact type definitions to frontend.
|
||||
- [ ] Keep live artifacts distinct from the static artifact model.
|
||||
- [ ] Do not add live artifacts as a static `ArtifactKind`.
|
||||
- [ ] Model workspace items as a discriminated `kind: 'live-artifact'` entry.
|
||||
- [ ] Use namespaced tab IDs such as `live:<artifactId>`.
|
||||
- [ ] Extend frontend provider/registry methods.
|
||||
- [ ] List live artifacts.
|
||||
- [ ] Fetch live artifact detail.
|
||||
- [ ] Build preview URL.
|
||||
- [ ] Add entry-header connector surface.
|
||||
- [ ] Add `Connectors` tab in `apps/web/src/components/EntryView.tsx`.
|
||||
- [ ] In Phase 1B, show stubbed or local-only connector cards until Phase 3 connector APIs are implemented.
|
||||
- [ ] Display connector name, category/provider, status, account label, and connect/configure actions as available.
|
||||
- [ ] Add new project live artifact entry.
|
||||
- [ ] Add `New live artifact` to new project tabs in `apps/web/src/components/NewProjectPanel.tsx`.
|
||||
- [ ] Start normal project creation with live-artifact intent / skill path.
|
||||
- [ ] List live artifacts in the `Designs` tab.
|
||||
- [ ] Show live artifacts as first-class selectable entries associated with their parent project/design.
|
||||
- [ ] Visually distinguish entries with `Live` / `Refreshable` status.
|
||||
- [ ] Open directly into the parent project workspace with the live artifact selected.
|
||||
- [ ] Optionally show live-artifact count/status on parent design cards.
|
||||
- [ ] Show live artifacts as virtual nodes in the artifact tree.
|
||||
- [ ] One node per live artifact.
|
||||
- [ ] Hide `snapshots/` by default.
|
||||
- [ ] Render under a virtual group in `FileWorkspace.tsx`.
|
||||
- [ ] Add badges.
|
||||
- [ ] `Live`.
|
||||
- [ ] `Refreshable`.
|
||||
- [ ] `Refreshing...`.
|
||||
- [ ] `Refresh failed`.
|
||||
- [ ] `Archived`.
|
||||
- [ ] Add preview panel support.
|
||||
- [ ] Load preview route in iframe.
|
||||
- [ ] Add `LiveArtifactViewer` path in `apps/web/src/components/FileViewer.tsx`.
|
||||
- [ ] Reuse existing `HtmlViewer` toolbar / iframe / sandbox patterns.
|
||||
- [ ] Add `Preview` tab.
|
||||
- [ ] Add `Source` tab.
|
||||
- [ ] Add `Data` tab.
|
||||
- [ ] Add `Provenance` tab.
|
||||
- [ ] Add `Refresh history` tab.
|
||||
- [ ] Ensure `data.json` is source of truth.
|
||||
- [ ] Treat any API `dataJson` as derived cache only.
|
||||
- [ ] On divergence, prefer `data.json`.
|
||||
|
||||
## Phase 1C — Built-in skill and wrapper CLI
|
||||
|
||||
- [ ] Add built-in skill directory.
|
||||
- [ ] `skills/live-artifact/SKILL.md`.
|
||||
- [ ] `skills/live-artifact/references/artifact-schema.md`.
|
||||
- [ ] `skills/live-artifact/references/connector-policy.md`.
|
||||
- [ ] `skills/live-artifact/references/refresh-contract.md`.
|
||||
- [ ] Write skill frontmatter.
|
||||
- [ ] `name: live-artifact`.
|
||||
- [ ] Trigger keywords for live dashboards/reports.
|
||||
- [ ] `od.mode: prototype`.
|
||||
- [ ] `od.preview.type: html`.
|
||||
- [ ] `od.outputs.primary: index.html`.
|
||||
- [ ] Required capabilities: `shell`, `file_write`.
|
||||
- [ ] Write skill workflow instructions.
|
||||
- [ ] Determine if user wants live vs static artifact.
|
||||
- [ ] Use daemon wrappers instead of raw curl.
|
||||
- [ ] Write `template.html`, `data.json`, `artifact.json`, `provenance.json`.
|
||||
- [ ] Include `template.html` in declared skill outputs.
|
||||
- [ ] Treat `index.html` as derived preview output.
|
||||
- [ ] Never store credentials or raw provider responses.
|
||||
- [ ] Keep `data.json` compact and preview-oriented.
|
||||
- [ ] Implement wrapper CLI.
|
||||
- [ ] `od tools live-artifacts create --input artifact.json`.
|
||||
- [ ] `od tools live-artifacts list --format compact`.
|
||||
- [ ] `od tools live-artifacts update --artifact-id <id> --input artifact.json`.
|
||||
- [ ] `od tools live-artifacts refresh --artifact-id <id>`.
|
||||
- [ ] Implement project-owned command code as TypeScript under `apps/daemon/src`.
|
||||
- [ ] Ensure wrapper reads injected environment.
|
||||
- [ ] `OD_DAEMON_URL`.
|
||||
- [ ] `OD_TOOL_TOKEN`.
|
||||
- [ ] Ensure wrapper returns agent-friendly output.
|
||||
- [ ] Compact success JSON.
|
||||
- [ ] Compact validation errors.
|
||||
- [ ] Non-zero exit code on failure.
|
||||
- [ ] Update system prompt / skill preamble injection.
|
||||
- [ ] Include daemon URL.
|
||||
- [ ] Include token availability without exposing unnecessary internals.
|
||||
- [ ] Verify skill discovery.
|
||||
- [ ] Skill appears in catalog.
|
||||
- [ ] Skill body includes root preamble for references/assets.
|
||||
- [ ] Active agent receives live artifact workflow instructions.
|
||||
|
||||
## Phase 2 — Refresh runner
|
||||
|
||||
- [ ] Implement refresh storage.
|
||||
- [ ] Append-only `refreshes.jsonl`.
|
||||
- [ ] Refresh step status.
|
||||
- [ ] Refresh duration.
|
||||
- [ ] Connector/tool metadata.
|
||||
- [ ] Compact error records.
|
||||
- [ ] Implement refresh lock.
|
||||
- [ ] One active refresh per artifact.
|
||||
- [ ] Reject or queue concurrent refreshes.
|
||||
- [ ] Implement monotonic refresh IDs.
|
||||
- [ ] Prevent stale refresh from overwriting newer committed data.
|
||||
- [ ] Implement timeout and cancellation.
|
||||
- [ ] Per-source timeout.
|
||||
- [ ] Total refresh timeout.
|
||||
- [ ] User cancellation path.
|
||||
- [ ] Implement crash recovery.
|
||||
- [ ] Detect stale `running` refreshes on daemon startup.
|
||||
- [ ] Mark timed-out stale runs as failed.
|
||||
- [ ] Preserve last valid preview.
|
||||
- [ ] Implement local daemon refresh sources.
|
||||
- [ ] `project_files.search`.
|
||||
- [ ] `project_files.read_json`.
|
||||
- [ ] `git.summary`.
|
||||
- [ ] Implement declarative output mapping.
|
||||
- [ ] `outputMapping.dataPaths`.
|
||||
- [ ] `identity` transform.
|
||||
- [ ] `compact_table` transform.
|
||||
- [ ] `metric_summary` transform.
|
||||
- [ ] Implement all-or-nothing refresh commit.
|
||||
- [ ] Build candidate `data.json`.
|
||||
- [ ] Validate all candidates.
|
||||
- [ ] Commit only if all refreshable sources succeed.
|
||||
- [ ] Keep previous data/preview if any source fails.
|
||||
- [ ] Implement snapshot behavior.
|
||||
- [ ] Successful snapshots under `snapshots/<refreshId>/`.
|
||||
- [ ] Failed attempts summarized in `refreshes.jsonl`.
|
||||
- [ ] Decide whether failed payloads are retained under hidden failed snapshot directory.
|
||||
- [ ] Add refresh UI.
|
||||
- [ ] Refresh button.
|
||||
- [ ] Running state.
|
||||
- [ ] Success state.
|
||||
- [ ] Failure state with actionable error.
|
||||
- [ ] Implement refresh permission flow.
|
||||
- [ ] Initial state `refreshPermission: none`.
|
||||
- [ ] First manual refresh confirmation.
|
||||
- [ ] Persist `manual_refresh_granted_for_read_only` after approval.
|
||||
- [ ] Allow revoke from Source tab.
|
||||
- [ ] Emit refresh events.
|
||||
- [ ] Extend `packages/contracts/src/sse/chat.ts` for live artifact create/update/refresh events.
|
||||
- [ ] Translate live artifact SSE payloads in `apps/web/src/providers/daemon.ts`.
|
||||
- [ ] Auto-open created/updated live artifacts via the existing project open-request flow.
|
||||
- [ ] Update viewer refresh loading/failure state.
|
||||
|
||||
## Phase 3 — Connector catalog and read-only connector tools
|
||||
|
||||
- [ ] Create connector module structure.
|
||||
- [ ] `apps/daemon/src/connectors/catalog.ts`.
|
||||
- [ ] `apps/daemon/src/connectors/service.ts`.
|
||||
- [ ] `apps/daemon/src/connectors/routes.ts`.
|
||||
- [ ] `apps/daemon/src/tools/connectors.ts`.
|
||||
- [ ] Implement connector catalog.
|
||||
- [ ] Static connector definitions.
|
||||
- [ ] Featured tools.
|
||||
- [ ] Allowed tools.
|
||||
- [ ] Minimum approval policy.
|
||||
- [ ] Implement connector status service.
|
||||
- [ ] `available`.
|
||||
- [ ] `connected`.
|
||||
- [ ] `error`.
|
||||
- [ ] `disabled`.
|
||||
- [ ] `accountLabel`.
|
||||
- [ ] Add connector UI/API endpoints.
|
||||
- [ ] `GET /api/connectors`.
|
||||
- [ ] `GET /api/connectors/:connectorId`.
|
||||
- [ ] `POST /api/connectors/:connectorId/connect`.
|
||||
- [ ] `DELETE /api/connectors/:connectorId/connection`.
|
||||
- [ ] Defer OAuth callback routes until OAuth-backed connectors are implemented.
|
||||
- [ ] Implement connector tool endpoints.
|
||||
- [ ] `GET /api/tools/connectors/list`.
|
||||
- [ ] `POST /api/tools/connectors/execute`.
|
||||
- [ ] Implement connector wrapper CLI.
|
||||
- [ ] `od tools connectors list --format compact`.
|
||||
- [ ] `od tools connectors execute --connector <id> --tool <name> --input input.json`.
|
||||
- [ ] Implement project-owned command code as TypeScript under `apps/daemon/src`.
|
||||
- [ ] Implement read-only safety classification.
|
||||
- [ ] Scope/name contains write/create/update/delete/admin/send/post/manage → write/confirm.
|
||||
- [ ] Destructive hints → destructive/disabled for refresh.
|
||||
- [ ] Explicit read-only hints → read/auto.
|
||||
- [ ] Unknown → write/confirm.
|
||||
- [ ] Enforce policy at execution time.
|
||||
- [ ] Re-check allowlist.
|
||||
- [ ] Re-check current scopes.
|
||||
- [ ] Re-check tool safety.
|
||||
- [ ] Re-check connector status.
|
||||
- [ ] Fail closed if read-only tool becomes write-capable.
|
||||
- [ ] Never make write/destructive/unknown tools refreshable.
|
||||
- [ ] Implement connector output protections.
|
||||
- [ ] Max serialized output size, e.g. 256KB.
|
||||
- [ ] Per-run rate limit, e.g. 10 calls/minute.
|
||||
- [ ] Per-run total call limit, e.g. 60 calls/run.
|
||||
- [ ] Redact credentials and provider envelope fields.
|
||||
- [ ] Return compact summaries where possible.
|
||||
- [ ] Implement credential storage policy.
|
||||
- [ ] Store OAuth credentials outside project artifacts.
|
||||
- [ ] Use daemon-controlled global store or app database.
|
||||
- [ ] Persist only connector references in artifacts.
|
||||
- [ ] Never write tokens/headers/cookies/OAuth state under live artifact directories.
|
||||
- [ ] Add first connector or public/local provider.
|
||||
- [ ] Prefer local/public source before OAuth-backed provider.
|
||||
- [ ] Validate read-only refresh end-to-end.
|
||||
- [ ] Update skill references.
|
||||
- [ ] Document connector listing.
|
||||
- [ ] Document connector execution.
|
||||
- [ ] Document read-only refresh rules.
|
||||
- [ ] Document credential handling constraints.
|
||||
|
||||
## Phase 4 — Optional MCP wrapper
|
||||
|
||||
- [x] Confirm need for MCP after skill + wrapper path works.
|
||||
- [x] Design MCP server as wrapper over existing daemon services.
|
||||
- [ ] Do not make MCP required.
|
||||
- [ ] Do not mutate user global MCP config automatically.
|
||||
- [ ] Keep skill + CLI path unchanged.
|
||||
- [ ] Add MCP discovery only for agents with mature MCP support.
|
||||
- [x] Verify Claude Code or another MCP-capable agent can discover equivalent tools.
|
||||
|
||||
## Cross-cutting verification
|
||||
|
||||
- [ ] Run schema tests.
|
||||
- [ ] Run daemon route tests.
|
||||
- [ ] Run storage/path traversal tests.
|
||||
- [ ] Run preview renderer tests.
|
||||
- [ ] Run token auth tests.
|
||||
- [ ] Run refresh failure fallback tests.
|
||||
- [ ] Run connector policy tests.
|
||||
- [ ] Run frontend build/typecheck.
|
||||
- [ ] Run an end-to-end local artifact create flow with one supported agent.
|
||||
- [ ] Run an end-to-end refresh flow using a local read-only source.
|
||||
- [ ] Verify no fixture or generated artifact contains raw credentials, headers, cookies, or full provider payloads.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "Invalid Credential-Like Fields",
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "index.html"
|
||||
},
|
||||
"document": {
|
||||
"format": "html_template_v1",
|
||||
"templatePath": "template.html",
|
||||
"generatedPreviewPath": "index.html",
|
||||
"dataPath": "data.json",
|
||||
"dataJson": {
|
||||
"title": "Invalid Credential-Like Fields",
|
||||
"account": "fixture-service"
|
||||
},
|
||||
"sourceJson": {
|
||||
"type": "connector_tool",
|
||||
"toolName": "fixture.read",
|
||||
"input": {
|
||||
"token": "REDACTED_FIXTURE_TOKEN",
|
||||
"password": "REDACTED_FIXTURE_PASSWORD"
|
||||
},
|
||||
"connector": {
|
||||
"connectorId": "fixture",
|
||||
"accountLabel": "fixture-service",
|
||||
"toolName": "fixture.read",
|
||||
"approvalPolicy": "read_only_auto"
|
||||
},
|
||||
"refreshPermission": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Invalid Credential-Like Fields",
|
||||
"account": "fixture-service"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{data.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{data.title}}</h1>
|
||||
<p>{{data.account}}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"title": "Invalid Raw Provider Fields",
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "index.html"
|
||||
},
|
||||
"document": {
|
||||
"format": "html_template_v1",
|
||||
"templatePath": "template.html",
|
||||
"generatedPreviewPath": "index.html",
|
||||
"dataPath": "data.json",
|
||||
"dataJson": {
|
||||
"title": "Invalid Raw Provider Fields",
|
||||
"rawResponse": {
|
||||
"status": 200,
|
||||
"payload": {
|
||||
"items": ["provider envelope should not be persisted"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Invalid Raw Provider Fields",
|
||||
"rawResponse": {
|
||||
"status": 200,
|
||||
"payload": {
|
||||
"items": ["provider envelope should not be persisted"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{data.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{data.title}}</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "live_static_minimal",
|
||||
"projectId": "project_fixture",
|
||||
"title": "Minimal Static Live Artifact",
|
||||
"slug": "minimal-static-live-artifact",
|
||||
"status": "active",
|
||||
"pinned": false,
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "index.html"
|
||||
},
|
||||
"refreshStatus": "never",
|
||||
"createdAt": "2026-04-29T12:00:00.000Z",
|
||||
"updatedAt": "2026-04-29T12:00:00.000Z",
|
||||
"document": {
|
||||
"format": "html_template_v1",
|
||||
"templatePath": "template.html",
|
||||
"generatedPreviewPath": "index.html",
|
||||
"dataPath": "data.json",
|
||||
"dataJson": {
|
||||
"title": "Minimal Static Live Artifact",
|
||||
"summary": "A safe static fixture rendered from template.html and data.json."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Minimal Static Live Artifact",
|
||||
"summary": "A safe static fixture rendered from template.html and data.json."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"generatedAt": "2026-04-29T12:00:00.000Z",
|
||||
"generatedBy": "agent",
|
||||
"notes": "Static fixture with no refresh source.",
|
||||
"sources": [
|
||||
{
|
||||
"label": "Fixture author input",
|
||||
"type": "user_input"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{data.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>{{data.title}}</h1>
|
||||
<p>{{data.summary}}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
987
specs/2026-04-29-live-artifacts/spec.md
Normal file
987
specs/2026-04-29-live-artifacts/spec.md
Normal file
@@ -0,0 +1,987 @@
|
||||
# Live Artifacts via Agent Skills
|
||||
|
||||
**Status:** Draft · 2026-04-29
|
||||
**Parent:** [`docs/spec.md`](../../docs/spec.md)
|
||||
**Siblings:** [`docs/skills-protocol.md`](../../docs/skills-protocol.md) · [`docs/agent-adapters.md`](../../docs/agent-adapters.md) · [`docs/modes.md`](../../docs/modes.md)
|
||||
**Reference implementation:** `~/Projects/monet` connectors + live artifacts
|
||||
|
||||
This spec defines how to bring Monet's **connectors** and **live artifacts** ideas into Open Design, but implement the agent-facing surface as **file-based agent skills plus daemon-owned local tools**, not as an in-process tool registry or MCP-first integration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Product goal
|
||||
|
||||
Open Design should let an agent create previewable artifacts that are not just one-off generated files, but **live, refreshable, auditable views** backed by external or local data sources.
|
||||
|
||||
Examples:
|
||||
|
||||
- “Create a live GitHub release dashboard.”
|
||||
- “Make a Notion project status page and let me refresh it tomorrow.”
|
||||
- “Turn this folder of JSON files into a polished stakeholder report.”
|
||||
- “Create a design-system coverage artifact that can be refreshed after code changes.”
|
||||
|
||||
The user experience should feel like the existing OD artifact flow:
|
||||
|
||||
1. User chats with the selected agent.
|
||||
2. Agent uses a skill to plan and create a live artifact.
|
||||
3. OD persists the artifact as project-scoped files and metadata.
|
||||
4. UI previews the artifact in the existing iframe/file viewer model.
|
||||
5. User can refresh the artifact later without asking the agent to redesign it from scratch.
|
||||
|
||||
## 2. Key decision
|
||||
|
||||
### 2.1 Use `skill + daemon tool endpoint`, not MCP-first
|
||||
|
||||
Monet exposes connectors and live artifacts through a controller-owned tool registry. OD should not copy that exact runtime shape because OD's core architecture is different: OD delegates to external CLI agents such as Claude Code, Codex, Cursor Agent, Gemini CLI, OpenCode, and Qwen.
|
||||
|
||||
The agent-facing interface should therefore be:
|
||||
|
||||
```text
|
||||
skills/live-artifact/SKILL.md
|
||||
↓ instructs the agent to call
|
||||
daemon local HTTP endpoints or wrapper CLI commands
|
||||
↓ backed by
|
||||
daemon-owned connector + artifact services
|
||||
↓ persisted as
|
||||
project workspace files + metadata
|
||||
```
|
||||
|
||||
MCP may be added later as a wrapper over the same daemon services, but it should not be the first or only interface.
|
||||
|
||||
Reasons:
|
||||
|
||||
- **Multi-agent compatibility:** every supported agent can read a skill and execute shell commands; MCP support varies by agent and CLI version.
|
||||
- **Lower migration cost:** current daemon `/api/chat` does not support per-run MCP binding.
|
||||
- **Centralized safety:** daemon endpoints can enforce project, path, connector, and output-size policies consistently.
|
||||
- **Skill-native product model:** OD's extension point is already `skills/` + `SKILL.md`, so live artifacts should feel like another OD capability, not a separate agent protocol.
|
||||
|
||||
### 2.2 Keep live artifacts distinct, but project-native
|
||||
|
||||
Live artifacts are a distinct persisted model integrated into the existing project UI. They must not be represented as a new static `ArtifactKind` in the existing artifact model, because they require ID-based identity, directory-shaped runtime storage, refresh/provenance history, connector permissions, locking, and server-rendered preview behavior.
|
||||
|
||||
Product terms:
|
||||
|
||||
- **Design / project:** the workspace container.
|
||||
- **Artifact:** a static generated file inside a design.
|
||||
- **Live artifact:** a refreshable, data-backed artifact inside a design.
|
||||
- **Connector:** an external or local data source available to live artifacts.
|
||||
|
||||
Implementation boundaries:
|
||||
|
||||
- Keep dedicated live-artifact storage under `.live-artifacts/`, dedicated `/api/live-artifacts/*` endpoints, and dedicated live-artifact DTOs in `packages/contracts`.
|
||||
- Reuse the existing project scope, workspace tabs, file tree, viewer primitives, chat SSE stream, and API error envelope so live artifacts feel native without polluting the simple static artifact path.
|
||||
- Do not expose `.live-artifacts/` through generic project file APIs; all mutation should go through live-artifact or tool endpoints.
|
||||
|
||||
## 3. What to migrate from Monet
|
||||
|
||||
### 3.1 Concepts to preserve
|
||||
|
||||
From `~/Projects/monet`:
|
||||
|
||||
- Static connector catalog plus dynamic connection status.
|
||||
- Connector tool safety classification.
|
||||
- Read-only-first connector policy.
|
||||
- Live artifact / tile / source / provenance separation.
|
||||
- HTML document template plus data-binding contract.
|
||||
- Declarative output mapping from tool output to `data.json` / render models.
|
||||
- Strict render JSON validation.
|
||||
- Refresh source validation before re-execution.
|
||||
- Refresh audit trail with step-level status.
|
||||
- Failure fallback: invalid refresh output should not blank the artifact.
|
||||
|
||||
### 3.2 Concepts to adapt
|
||||
|
||||
Monet concept | OD adaptation
|
||||
---|---
|
||||
Controller `ToolRegistry` | Daemon service endpoints and optional CLI wrappers
|
||||
Chat tools `create_live_artifact`, `update_live_artifact`, `list_live_artifacts` | Skill instructions that call `od-tools live-artifacts ...` or localhost daemon endpoints
|
||||
Connector tools dynamically injected into tool registry | Connector catalog exposed through daemon endpoints; skill asks agent to query/use them explicitly
|
||||
SQLite-first artifact storage | Project-scoped metadata files first; SQLite optional later if indexing becomes necessary
|
||||
Controller-owned agent loop | External CLI agent loop; OD only injects skills and receives output/events
|
||||
|
||||
### 3.3 Monet files used as source material
|
||||
|
||||
- `apps/controller/src/connectors/catalog.ts`
|
||||
- `apps/controller/src/connectors/service.ts`
|
||||
- `apps/controller/src/routes/connectors.ts`
|
||||
- `apps/controller/src/tools/connectors.ts`
|
||||
- `apps/controller/src/live-artifacts/schema.ts`
|
||||
- `apps/controller/src/live-artifacts/render.ts`
|
||||
- `apps/controller/src/live-artifacts/refresh.ts`
|
||||
- `apps/controller/src/routes/live-artifacts.ts`
|
||||
- `apps/controller/src/tools/live-artifacts.ts`
|
||||
- `apps/controller/src/chat-storage.ts`
|
||||
- `specs/2026-04-27-live-artifacts/spec.md`
|
||||
|
||||
## 4. Target architecture
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Web App │
|
||||
│ chat · artifact tree · live artifact list · refresh button │
|
||||
│ iframe preview · source/provenance panels │
|
||||
└───────────────┬──────────────────────────────────────────────────┘
|
||||
│ HTTP/SSE
|
||||
┌───────────────▼──────────────────────────────────────────────────┐
|
||||
│ Local Daemon │
|
||||
│ │
|
||||
│ Agent session broker │
|
||||
│ Skill registry │
|
||||
│ Built-in tool endpoints │
|
||||
│ /api/tools/live-artifacts/* │
|
||||
│ /api/tools/connectors/* │
|
||||
│ /api/connectors/* │
|
||||
│ Artifact store │
|
||||
│ Connector service │
|
||||
│ Refresh runner + audit log │
|
||||
└───────────────┬──────────────────────────────────────────────────┘
|
||||
│ spawn / stdio
|
||||
┌───────────────▼──────────────────────────────────────────────────┐
|
||||
│ External Agent CLI │
|
||||
│ Claude Code · Codex · Cursor Agent · Gemini CLI · OpenCode · Qwen │
|
||||
│ │
|
||||
│ Receives SKILL.md instructions and calls daemon tools via shell │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 5. User-facing skill shape
|
||||
|
||||
Add a built-in skill:
|
||||
|
||||
```text
|
||||
skills/live-artifact/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ ├── artifact-schema.md
|
||||
│ ├── connector-policy.md
|
||||
│ └── refresh-contract.md
|
||||
└── assets/
|
||||
└── templates/
|
||||
├── dashboard.html
|
||||
└── report.html
|
||||
```
|
||||
|
||||
### 5.1 `SKILL.md` frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: live-artifact
|
||||
description: |
|
||||
Create refreshable, auditable Open Design artifacts backed by connector or local data.
|
||||
Trigger when the user asks for live dashboards, refreshable reports, synced views, or reusable data-backed artifacts.
|
||||
triggers:
|
||||
- live artifact
|
||||
- refreshable dashboard
|
||||
- live report
|
||||
- synced view
|
||||
- 可刷新
|
||||
- 实时看板
|
||||
od:
|
||||
mode: prototype
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
reload: debounce-100
|
||||
design_system:
|
||||
requires: true
|
||||
outputs:
|
||||
primary: index.html
|
||||
secondary:
|
||||
- template.html
|
||||
- artifact.json
|
||||
- data.json
|
||||
- provenance.json
|
||||
capabilities_required:
|
||||
- shell
|
||||
- file_write
|
||||
---
|
||||
```
|
||||
|
||||
### 5.2 Skill body responsibilities
|
||||
|
||||
The skill should instruct the agent to:
|
||||
|
||||
1. Determine whether the user wants a live artifact or a normal static artifact.
|
||||
2. Query available connectors and allowed read-only operations.
|
||||
3. Fetch from the named connected connector/source when available; ask for a data source only when no matching connected source exists, multiple candidates are equally plausible, or the request lacks any searchable topic/page/database clue.
|
||||
4. Create a safe render model, not raw provider output.
|
||||
5. Write `template.html`, `data.json`, `artifact.json`, and `provenance.json` into the live artifact workspace directory; treat `index.html` as derived preview output.
|
||||
6. Register the artifact through daemon tooling.
|
||||
7. Include provenance and refresh source metadata.
|
||||
8. Never store credentials, raw OAuth responses, headers, cookies, or tokens.
|
||||
|
||||
### 5.3 Agent-callable command surface
|
||||
|
||||
Prefer a small `od` wrapper command over raw `curl` in the skill body:
|
||||
|
||||
```bash
|
||||
od tools live-artifacts create --input artifact.json
|
||||
od tools live-artifacts list --format compact
|
||||
od tools live-artifacts update --artifact-id "$ID" --input artifact.json
|
||||
od tools live-artifacts refresh --artifact-id "$ID"
|
||||
od tools connectors list --format compact
|
||||
od tools connectors execute --connector github --tool list_releases --input input.json
|
||||
```
|
||||
|
||||
The wrapper should be implemented as TypeScript source under `apps/daemon/src` and call daemon endpoints using injected runtime values:
|
||||
|
||||
- `OD_DAEMON_URL`
|
||||
- `OD_TOOL_TOKEN`
|
||||
|
||||
The daemon injects these into the system prompt or skill preamble at runtime. The agent should not choose or override `projectId`; `/api/tools/*` derives project/run scope from `OD_TOOL_TOKEN`. If standalone JavaScript wrappers are later exposed, they must be generated build output from TypeScript source, not project-owned `.js` source files.
|
||||
|
||||
Raw HTTP is for developer debugging only and must include the run-scoped bearer token:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$OD_DAEMON_URL/api/tools/live-artifacts/create" \
|
||||
-H 'content-type: application/json' \
|
||||
-H "authorization: Bearer $OD_TOOL_TOKEN" \
|
||||
-d @artifact.json
|
||||
```
|
||||
|
||||
## 6. Daemon API design
|
||||
|
||||
### 6.1 Connector endpoints
|
||||
|
||||
```http
|
||||
GET /api/connectors
|
||||
GET /api/connectors/:connectorId
|
||||
POST /api/connectors/:connectorId/connect
|
||||
DELETE /api/connectors/:connectorId/connection
|
||||
```
|
||||
|
||||
MVP may stub OAuth-backed connectors and start with local/read-only connectors, but the API should preserve Monet's split between catalog and connection status. OAuth callback routes are deferred until OAuth-backed connectors are implemented.
|
||||
|
||||
Connector response shape:
|
||||
|
||||
```ts
|
||||
type ConnectorDetail = {
|
||||
id: string;
|
||||
label: string;
|
||||
category: 'code' | 'docs' | 'files' | 'analytics' | 'custom';
|
||||
status: 'available' | 'connected' | 'error' | 'disabled';
|
||||
accountLabel?: string;
|
||||
featuredTools: ConnectorToolSummary[];
|
||||
allowedTools: ConnectorToolSummary[];
|
||||
minimumApprovalPolicy: 'read_only_auto' | 'confirm_write' | 'disabled';
|
||||
errorCode?: string;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 Connector tool endpoints
|
||||
|
||||
Agent and refresh-runner connector execution must use the same daemon-owned execution path:
|
||||
|
||||
```http
|
||||
GET /api/tools/connectors/list
|
||||
POST /api/tools/connectors/execute
|
||||
```
|
||||
|
||||
`/api/tools/connectors/list` returns a compact list of connected, allowed, read-only-first tools for the current run token.
|
||||
|
||||
`/api/tools/connectors/execute` request:
|
||||
|
||||
```ts
|
||||
type ConnectorExecuteRequest = {
|
||||
connectorId: string;
|
||||
toolName: string;
|
||||
input: BoundedJsonObject;
|
||||
purpose: 'agent_preview' | 'artifact_refresh';
|
||||
};
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```ts
|
||||
type ConnectorExecuteResponse =
|
||||
| {
|
||||
ok: true;
|
||||
connectorId: string;
|
||||
accountLabel?: string;
|
||||
toolName: string;
|
||||
safety: ConnectorToolSafety;
|
||||
output: BoundedJsonValue;
|
||||
outputSummary?: string;
|
||||
providerExecutionId?: string;
|
||||
metadata?: BoundedJsonObject;
|
||||
}
|
||||
| ApiErrorResponse;
|
||||
```
|
||||
|
||||
Execution rules:
|
||||
|
||||
- Require a valid `OD_TOOL_TOKEN` bound to the active run/project.
|
||||
- Reject tools that are not in the connector catalog allowlist.
|
||||
- Re-classify tool safety at execution time; catalog metadata alone is not authorization.
|
||||
- Reject `write`, `destructive`, and `unknown` tools for `artifact_refresh`.
|
||||
- Bound output size before it is returned to the agent.
|
||||
- Redact credentials and raw provider envelope fields before returning or persisting anything.
|
||||
- Record `providerExecutionId`, connector/account labels, and safety policy for provenance.
|
||||
|
||||
### 6.3 Live artifact endpoints
|
||||
|
||||
Agent/tool endpoints:
|
||||
|
||||
```http
|
||||
POST /api/tools/live-artifacts/create
|
||||
GET /api/tools/live-artifacts/list
|
||||
POST /api/tools/live-artifacts/update
|
||||
POST /api/tools/live-artifacts/refresh
|
||||
```
|
||||
|
||||
UI endpoints:
|
||||
|
||||
```http
|
||||
GET /api/live-artifacts?projectId=...
|
||||
POST /api/live-artifacts
|
||||
GET /api/live-artifacts/:artifactId
|
||||
PATCH /api/live-artifacts/:artifactId
|
||||
POST /api/live-artifacts/:artifactId/refresh
|
||||
GET /api/live-artifacts/:artifactId/preview
|
||||
```
|
||||
|
||||
The `/api/tools/*` endpoints are optimized for agent consumption: compact JSON, concise errors, and explicit machine-readable validation failures. They never accept an arbitrary `projectId`; project/run scope comes from `OD_TOOL_TOKEN`. The `/api/live-artifacts/*` endpoints are optimized for UI state and use the web app's normal project context.
|
||||
|
||||
Both endpoint families must call the same service-layer validation and storage code. Only authentication and response verbosity should differ; errors should share the `ApiErrorResponse` envelope from `packages/contracts`.
|
||||
|
||||
Agent-facing tool endpoints should reuse the shared API error envelope from `packages/contracts/src/errors.ts` instead of introducing a parallel error type:
|
||||
|
||||
```ts
|
||||
type LiveArtifactToolResponse<TSuccess> = TSuccess | ApiErrorResponse;
|
||||
```
|
||||
|
||||
Add live-artifact and connector-specific `ApiErrorCode` values such as `TOOL_TOKEN_INVALID`, `TOOL_TOKEN_EXPIRED`, `CONNECTOR_NOT_CONNECTED`, `CONNECTOR_SAFETY_DENIED`, `REFRESH_LOCKED`, `REFRESH_TIMED_OUT`, `OUTPUT_TOO_LARGE`, `TEMPLATE_BINDING_INVALID`, and `REDACTION_REQUIRED`. Validation details should live in the existing error `details` field so web, daemon, and tests share one error model.
|
||||
|
||||
## 7. Data model
|
||||
|
||||
### 7.1 Storage layout
|
||||
|
||||
Use project-scoped files under the daemon runtime data directory first. `OD_DATA_DIR` may override the default; otherwise `<RUNTIME_DATA_DIR>` is `<repo>/.od`:
|
||||
|
||||
```text
|
||||
<RUNTIME_DATA_DIR>/projects/<projectId>/
|
||||
└── .live-artifacts/
|
||||
└── <artifactId>/
|
||||
├── artifact.json
|
||||
├── template.html
|
||||
├── index.html
|
||||
├── data.json
|
||||
├── provenance.json
|
||||
├── refreshes.jsonl
|
||||
└── snapshots/
|
||||
└── <refreshId>/
|
||||
├── data.json
|
||||
└── provenance.json
|
||||
```
|
||||
|
||||
The dot-prefixed `.live-artifacts/` directory keeps implementation files out of the generic project file tree while preserving OD's file-first, inspectable-on-disk artifact philosophy. Add SQLite later only for cross-project indexing or high-volume refresh history.
|
||||
|
||||
`index.html` is a generated preview artifact, not the source of truth for refreshable data. The UI should load live artifacts through:
|
||||
|
||||
```http
|
||||
GET /api/live-artifacts/:artifactId/preview
|
||||
```
|
||||
|
||||
The preview route may serve the stored `index.html` for static cases, but for refreshable HTML it should render `template.html + data.json` and apply iframe sandbox/CSP headers. `snapshots/` should be hidden from the normal artifact tree unless the user explicitly opens refresh history.
|
||||
|
||||
### 7.2 Core types
|
||||
|
||||
```ts
|
||||
type BoundedJsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| BoundedJsonValue[]
|
||||
| { [key: string]: BoundedJsonValue };
|
||||
|
||||
type BoundedJsonObject = { [key: string]: BoundedJsonValue };
|
||||
|
||||
type LiveArtifact = {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
projectId: string;
|
||||
sessionId?: string;
|
||||
createdByRunId?: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
status: 'active' | 'archived' | 'error';
|
||||
pinned: boolean;
|
||||
preview: {
|
||||
type: 'html' | 'jsx' | 'markdown';
|
||||
entry: string;
|
||||
};
|
||||
refreshStatus: 'never' | 'idle' | 'running' | 'succeeded' | 'failed';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastRefreshedAt?: string;
|
||||
document?: LiveArtifactDocument;
|
||||
};
|
||||
|
||||
type LiveArtifactDocument = {
|
||||
format: 'html_template_v1';
|
||||
templatePath: 'template.html';
|
||||
generatedPreviewPath: 'index.html';
|
||||
dataPath: 'data.json';
|
||||
dataJson: BoundedJsonObject;
|
||||
dataSchemaJson?: BoundedJsonObject;
|
||||
sourceJson?: LiveArtifactSource;
|
||||
};
|
||||
|
||||
type LiveArtifactSource = {
|
||||
type: 'local_file' | 'daemon_tool' | 'connector_tool';
|
||||
toolName?: string;
|
||||
input: BoundedJsonObject;
|
||||
connector?: {
|
||||
connectorId: string;
|
||||
accountLabel?: string;
|
||||
toolName: string;
|
||||
approvalPolicy: 'read_only_auto' | 'manual_refresh_granted_for_read_only';
|
||||
};
|
||||
outputMapping?: {
|
||||
dataPaths?: Array<{ from: string; to: string }>;
|
||||
transform?: 'identity' | 'compact_table' | 'metric_summary';
|
||||
};
|
||||
refreshPermission: 'none' | 'manual_refresh_granted_for_read_only';
|
||||
};
|
||||
|
||||
type LiveArtifactProvenance = {
|
||||
generatedAt: string;
|
||||
generatedBy: 'agent' | 'refresh_runner';
|
||||
notes?: string;
|
||||
sources: Array<{
|
||||
label: string;
|
||||
type: 'connector' | 'local_file' | 'user_input' | 'derived';
|
||||
ref?: string;
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 Validation rules
|
||||
|
||||
Port Monet's strict validation posture:
|
||||
|
||||
- Apply the shared bounded JSON constraints in `packages/contracts` to every persisted or agent-supplied `BoundedJsonValue` / `BoundedJsonObject`.
|
||||
- Reject keys such as `raw`, `rawResponse`, `payload`, `body`, `headers`, `cookie`, `authorization`, `token`, `secret`, `credential`, `password`.
|
||||
- Redact suspicious source inputs before persistence.
|
||||
- Reject source inputs that still contain credential-like values after redaction.
|
||||
- HTML preview files must be generated from the document contract; refresh updates `data.json`, not arbitrary script.
|
||||
|
||||
#### 7.3.1 Shared bounded JSON constraints
|
||||
|
||||
The shared live-artifact JSON envelope is intentionally small enough to validate synchronously, store in project files, display in the UI, and include in agent-facing error details without leaking raw provider payloads.
|
||||
|
||||
Define and export these constants from `packages/contracts` as `LIVE_ARTIFACT_BOUNDED_JSON_CONSTRAINTS`:
|
||||
|
||||
| Constraint | Value | Applies to |
|
||||
| --- | ---: | --- |
|
||||
| Maximum object/array depth | `8` | Any `BoundedJsonValue`; count the root object or array as depth `1`. |
|
||||
| Maximum object keys | `100` | Any single object. |
|
||||
| Maximum array length | `500` | Any single array. |
|
||||
| Maximum string length | `16 KiB` | Any single string value, measured in UTF-16 code units before persistence. |
|
||||
| Maximum serialized payload size | `256 KiB` | Any complete bounded JSON document, measured as UTF-8 bytes of canonical `JSON.stringify` output. |
|
||||
|
||||
Validation must fail closed when a value exceeds any limit. Persisted files and create/update inputs must use the same limits so valid agent input remains valid after storage round-trips. Future connector-specific limits may be stricter, but must not exceed this shared envelope for values persisted into live artifact files.
|
||||
|
||||
### 7.4 HTML document model
|
||||
|
||||
MVP live HTML artifacts should use `html_template_v1`:
|
||||
|
||||
```text
|
||||
template.html + data.json → daemon render step → index.html / preview response
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `template.html` is authored by the agent during create/update.
|
||||
- Refreshable values must come from `data.json`, not be hardcoded only in HTML.
|
||||
- `html_template_v1` supports **Mustache-style escaped interpolation plus a minimal `data-od-repeat` structural directive**. It does not support arbitrary JavaScript, expression evaluation, helper functions, filters, partials, or raw HTML injection.
|
||||
- Refresh updates `data.json` and snapshots. It does not let connector output rewrite arbitrary HTML.
|
||||
- If a presentation redesign is needed, the user should ask the agent to update the artifact; refresh is for data changes.
|
||||
- `index.html` may be regenerated after successful refresh, but it is derived output.
|
||||
- The preview route must serve the document in a sandboxed iframe context with a restrictive CSP. External scripts are disallowed in MVP unless vendored and allowlisted.
|
||||
|
||||
#### 7.4.1 `html_template_v1` binding contract
|
||||
|
||||
MVP binding syntax is intentionally small and deterministic:
|
||||
|
||||
- **Escaped interpolation:** `{{data.path.to.value}}` inserts a scalar value from `data.json`.
|
||||
- Paths must start with `data` and use dot-separated object keys, e.g. `{{data.summary.title}}`.
|
||||
- Numeric array indexes are allowed only as path segments, e.g. `{{data.metrics.0.value}}`.
|
||||
- Keys must match `/^[A-Za-z_][A-Za-z0-9_-]*$/`; bracket notation, computed paths, wildcards, function calls, and expressions are invalid.
|
||||
- Values render as strings. `null` and missing values render as an empty string. Objects and arrays are invalid interpolation targets except inside a supported repeat context.
|
||||
- **Repeat directive:** `data-od-repeat="item in data.items"` repeats the annotated element once for each object in an array.
|
||||
- The left side is a local alias matching `/^[A-Za-z_][A-Za-z0-9_]*$/`.
|
||||
- The right side must be a `data.*` path resolving to an array.
|
||||
- Inside the repeated element, interpolation may reference the local alias, e.g. `{{item.name}}`, using the same path grammar.
|
||||
- Nested `data-od-repeat` directives are disallowed in MVP.
|
||||
- `data-od-repeat` is removed from the generated output.
|
||||
- **Conditional directives:** none in MVP. Optional sections should be represented by empty strings, zero-length arrays, or separate agent-authored template variants during update.
|
||||
- **Attribute bindings:** interpolation may appear in text nodes and ordinary HTML attribute values, but not in attribute names, tag names, comments, `<script>`, `<style>`, `<iframe srcdoc>`, event-handler attributes such as `onclick`, or URL-bearing attributes that fail URL validation.
|
||||
|
||||
All interpolation is HTML-escaped by default:
|
||||
|
||||
- Text-node interpolation escapes `&`, `<`, `>`, `"`, and `'`.
|
||||
- Attribute interpolation escapes the same characters and must not be allowed to break out of the containing attribute.
|
||||
- URL-bearing attributes such as `href` and `src` must resolve to allowed `http:` or `https:` URLs, root-relative paths, same-artifact relative asset paths, or fragments; `javascript:`, `data:`, `blob:`, and other unsupported schemes are rejected.
|
||||
|
||||
Raw / unescaped interpolation is explicitly forbidden in MVP:
|
||||
|
||||
- No triple braces such as `{{{data.html}}}`.
|
||||
- No ampersand form such as `{{& data.html}}`.
|
||||
- No `data-od-html`, `data-od-raw`, `data-od-bind-html`, or equivalent raw insertion attributes.
|
||||
- No opt-out flag in artifact metadata or tool input.
|
||||
|
||||
The daemon must validate `template.html` before persistence and again before preview rendering. Validation failures must use the shared API error envelope with field/path details in `details`.
|
||||
|
||||
## 8. Runtime flows
|
||||
|
||||
### 8.1 Create flow
|
||||
|
||||
```text
|
||||
User asks for a live dashboard
|
||||
↓
|
||||
OD selects/injects live-artifact skill
|
||||
↓
|
||||
Agent queries connectors or local sources through daemon wrapper
|
||||
↓
|
||||
Agent writes template.html + artifact.json + data.json + provenance.json
|
||||
↓
|
||||
Agent calls live-artifacts create endpoint
|
||||
↓
|
||||
Daemon validates schemas, source metadata, file paths, and template binding
|
||||
↓
|
||||
Daemon stores artifact metadata and returns compact summary
|
||||
↓
|
||||
Web UI opens /api/live-artifacts/:artifactId/preview
|
||||
```
|
||||
|
||||
### 8.2 Refresh flow
|
||||
|
||||
```text
|
||||
User clicks Refresh
|
||||
↓
|
||||
UI POST /api/live-artifacts/:id/refresh
|
||||
↓
|
||||
Daemon loads artifact.json and the refreshable document source
|
||||
↓
|
||||
For the document source:
|
||||
- verify refreshPermission
|
||||
- verify connector still connected
|
||||
- verify tool is still read-only
|
||||
- verify accountLabel/connectorId did not drift
|
||||
- verify saved input matches current schema
|
||||
- execute source
|
||||
- map output through declarative outputMapping.dataPaths
|
||||
- update candidate dataJson
|
||||
- validate sanitized data
|
||||
↓
|
||||
Write refresh step to refreshes.jsonl
|
||||
↓
|
||||
If the refreshable source succeeds, commit data.json, snapshot, and regenerated preview
|
||||
If it fails, keep previous data/preview, write failed refresh record, and surface the error
|
||||
```
|
||||
|
||||
MVP refresh is **artifact-level all-or-nothing**.
|
||||
|
||||
Refresh runner requirements:
|
||||
|
||||
- Acquire a per-artifact refresh lock. Reject or queue concurrent refreshes.
|
||||
- Assign a monotonic `refreshId`; stale refreshes cannot overwrite newer committed data.
|
||||
- Enforce per-source and total refresh timeouts.
|
||||
- Persist every step to `refreshes.jsonl` with status, duration, connector metadata, and compact error.
|
||||
- On daemon restart, recover refreshes stuck in `running` past timeout as `failed` and keep the last valid preview.
|
||||
- Write snapshots only after validation succeeds, or write failed attempts under a separate `snapshots/<refreshId>/failed/` directory that is not used for preview.
|
||||
|
||||
### 8.3 Update flow
|
||||
|
||||
Agent or UI can update title, pinned status, archive status, preview entry, or non-source presentation fields. Updating `sourceJson` requires the same validation as create.
|
||||
|
||||
## 9. Connector strategy
|
||||
|
||||
### 9.1 Read-only v1
|
||||
|
||||
MVP should only expose read-only connector tools to automatic or refresh execution.
|
||||
|
||||
Write actions can exist later, but they must require explicit user confirmation and should not be refreshable.
|
||||
|
||||
Safety classification:
|
||||
|
||||
```ts
|
||||
type ConnectorToolSafety = {
|
||||
sideEffect: 'read' | 'write' | 'destructive' | 'unknown';
|
||||
approval: 'auto' | 'confirm' | 'disabled';
|
||||
reason: string;
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- OAuth scopes or names containing `write`, `create`, `update`, `delete`, `admin`, `send`, `post`, `manage` imply write/confirm.
|
||||
- Destructive hints imply destructive/disabled for refresh.
|
||||
- Explicit read-only hints can be read/auto.
|
||||
- Unknown defaults to write/confirm, not read/auto.
|
||||
|
||||
### 9.2 Execute-time enforcement
|
||||
|
||||
Connector policy must be enforced at execution and refresh time, not only when the catalog is built:
|
||||
|
||||
- Catalog classification is metadata, not authorization.
|
||||
- `/api/tools/connectors/execute` re-checks allowlist, current scopes, tool safety, and connector status.
|
||||
- Saved artifact policy cannot grant new permission by itself.
|
||||
- `unknown`, `write`, and `destructive` tools are never refreshable.
|
||||
- If a previously read-only tool later appears write-capable because scopes or provider metadata changed, refresh must fail closed.
|
||||
- A write action may be supported in the future with explicit confirmation, but it must not be stored as a refreshable source.
|
||||
- Agent calls and refresh-runner calls must share the same connector execution service so audit and safety behavior cannot drift.
|
||||
|
||||
### 9.3 Credential storage
|
||||
|
||||
Default decision:
|
||||
|
||||
- OAuth connection state and credentials live outside project artifacts, under a daemon-controlled global store such as `~/.open-design/connectors/` or an app database.
|
||||
- Project artifacts only store stable references: `connectorId`, `accountLabel`, provider tool id/name, minimized input, and provenance.
|
||||
- Access tokens, refresh tokens, headers, cookies, OAuth state, and raw provider responses are never written under `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts` or any other project artifact directory.
|
||||
- Refresh resolves credentials through the daemon connector service at execution time.
|
||||
- UI must show the connector/account label so users understand which global connection backs a project artifact.
|
||||
|
||||
### 9.4 Initial connector candidates
|
||||
|
||||
MVP can avoid OAuth complexity by shipping local daemon tools first:
|
||||
|
||||
- `project_files.search`
|
||||
- `project_files.read_json`
|
||||
- `git.summary`
|
||||
- `github.public_repo_summary` using unauthenticated public API or user-provided token later
|
||||
|
||||
OAuth-backed providers can follow the Monet pattern after the artifact pipeline is stable:
|
||||
|
||||
- GitHub
|
||||
- Notion
|
||||
- Google Drive
|
||||
- Linear
|
||||
|
||||
## 10. UI changes
|
||||
|
||||
### 10.1 Entry header connector tab
|
||||
|
||||
Add a `Connectors` tab to the entry-header navigation. When selected, it should show cards for available external and local connectors.
|
||||
|
||||
Connector cards should include, as data becomes available:
|
||||
|
||||
- connector name and provider/category
|
||||
- connection status
|
||||
- connected account label, when connected
|
||||
- connect, disconnect, or configure action
|
||||
- available read-only tools/capabilities, when useful
|
||||
|
||||
This tab is a workspace-level connector management surface, not a separate project type. Phase 1B may show stubbed or local-only connector card data; full external connector status, connect/disconnect actions, and OAuth-backed flows belong to the connector phase.
|
||||
|
||||
### 10.2 New project live artifact entry
|
||||
|
||||
Add `New live artifact` to the new project tabs. Selecting it should start the normal project creation flow with live-artifact intent and the `live-artifact` skill path preselected or strongly hinted.
|
||||
|
||||
This creates a normal project/design whose first output is expected to be a live artifact. It should not create a separate top-level project type unless the product model changes later.
|
||||
|
||||
Live artifacts should also be listed in the existing `Designs` tab. The `Designs` tab should continue to show normal designs/projects, and it should additionally surface live artifacts as first-class selectable entries associated with their parent project/design. Live artifact entries should be visually distinguishable with `Live` / `Refreshable` status and should open directly into the project workspace with the corresponding live artifact selected. Parent design cards may also show a small live-artifact count or status summary.
|
||||
|
||||
### 10.3 Artifact tree
|
||||
|
||||
Show live artifacts as a first-class virtual group in the existing artifact tree, not as raw nested files. The tree should show one node per live artifact and hide implementation files such as `snapshots/` by default.
|
||||
|
||||
`ProjectView.tsx` should fetch `GET /api/live-artifacts?projectId=...` alongside the existing project file fetch, then merge live artifacts into the workspace state as a discriminated item such as `kind: 'live-artifact'`. Use namespaced tab IDs such as `live:<artifactId>` so live artifacts cannot collide with file paths. `FileWorkspace.tsx` should render these items in a virtual group and route open/select actions to a live artifact viewer without treating artifact IDs as normal project file paths.
|
||||
|
||||
Badges:
|
||||
|
||||
- `Live`
|
||||
- `Refreshable`
|
||||
- `Refreshing...`
|
||||
- `Refresh failed`
|
||||
- `Archived`
|
||||
|
||||
### 10.4 Preview panel
|
||||
|
||||
Reuse existing iframe/file viewer where possible:
|
||||
|
||||
- Load `GET /api/live-artifacts/:artifactId/preview` in the iframe instead of opening nested files directly.
|
||||
- Add read-only viewer toolbar tabs using the existing `FileViewer.tsx` / `HtmlViewer` tab pattern: `Preview`, `Source`, `Data`, `Provenance`, `Refresh history`. Do not require a new split-pane side panel in Phase 1B.
|
||||
- Show refresh button only when at least one tile is refreshable. When `refreshStatus` is `running`, the button should be disabled and show a loading state to prevent duplicate refreshes.
|
||||
|
||||
Data sources for viewer tabs:
|
||||
|
||||
- `Source`: `artifact.json` `sourceJson` fields with credentials redacted.
|
||||
- `Data`: current `data.json` and tile render summaries.
|
||||
- `Provenance`: `provenance.json` and connector/account labels.
|
||||
- `Refresh history`: parsed `refreshes.jsonl`, newest first.
|
||||
|
||||
### 10.5 Chat integration
|
||||
|
||||
When an agent creates or updates a live artifact, the daemon should emit an agent/UI event similar to produced files so the UI can open it automatically. For MVP, extend the existing chat SSE stream in `packages/contracts/src/sse/chat.ts` rather than creating a second live-artifact SSE connection. `apps/web/src/providers/daemon.ts` should translate the SSE payload into a UI `AgentEvent` such as `kind: 'live_artifact_update'`, and `ProjectView.tsx` should use that event to refresh the live artifact list and auto-open the artifact using the same open-request flow used for produced files.
|
||||
|
||||
Suggested event:
|
||||
|
||||
```ts
|
||||
type LiveArtifactEvent = {
|
||||
type: 'live_artifact_created' | 'live_artifact_updated' | 'live_artifact_refresh_completed';
|
||||
artifactId: string;
|
||||
title: string;
|
||||
previewUrl?: string;
|
||||
status: string;
|
||||
};
|
||||
```
|
||||
|
||||
## 11. Implementation plan
|
||||
|
||||
### Phase 0 — Contracts first
|
||||
|
||||
- Add this spec.
|
||||
- Add shared TypeScript DTOs under `packages/contracts`, keeping the package pure TypeScript and free of Next.js, Express, filesystem/process APIs, browser APIs, SQLite, daemon internals, and sidecar control-plane dependencies.
|
||||
- Add shared contract DTOs for `LiveArtifact`, `LiveArtifactSource`, and connector catalog entries. Runtime validation schemas belong under daemon source, especially `apps/daemon/src/live-artifacts/schema.ts`, and should consume or mirror the shared contract types without adding daemon internals to `packages/contracts`.
|
||||
- Add or update contract files such as:
|
||||
- `packages/contracts/src/api/live-artifacts.ts`
|
||||
- `packages/contracts/src/api/connectors.ts`
|
||||
- `packages/contracts/src/sse/chat.ts`
|
||||
- `packages/contracts/src/examples.ts`
|
||||
- `packages/contracts/src/index.ts`
|
||||
- Add fixture artifacts under `specs/2026-04-29-live-artifacts/examples/`.
|
||||
- Extend `packages/contracts/src/errors.ts` with live artifact / connector error codes instead of defining a second error envelope.
|
||||
- Define `html_template_v1` binding grammar and example `template.html + data.json`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Schemas reject raw provider response fields and credential-like values.
|
||||
- Example artifact can be rendered from `template.html + data.json` through the preview contract.
|
||||
|
||||
### Phase 1A — Register static local live artifacts
|
||||
|
||||
- Implement daemon live artifact service.
|
||||
- Implement project-scoped file storage under `<RUNTIME_DATA_DIR>/projects/<projectId>/.live-artifacts`.
|
||||
- Add `/api/tools/live-artifacts/create` and `list`.
|
||||
- Add `GET /api/live-artifacts?projectId=...` and `GET /api/live-artifacts/:artifactId`.
|
||||
- Add run-scoped `OD_TOOL_TOKEN` for tool endpoints.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A static `html_template_v1` artifact can be registered without connectors or refresh.
|
||||
- The daemon rejects invalid paths, raw provider fields, and credential-like values.
|
||||
|
||||
### Phase 1B — UI preview integration
|
||||
|
||||
- Add `GET /api/live-artifacts/:artifactId/preview`.
|
||||
- Render `template.html + data.json` into a sandboxed iframe response.
|
||||
- Fetch live artifacts alongside project files and show them as virtual nodes in the artifact tree.
|
||||
- Add a `LiveArtifactViewer` path in `FileViewer.tsx` that reuses the existing HTML viewer toolbar/iframe patterns.
|
||||
- Add read-only `Preview`, `Source`, `Data`, `Provenance`, and `Refresh history` viewer tabs.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- UI can list and preview a registered live artifact.
|
||||
- Preview does not require exposing `snapshots/` or implementation files as normal project files.
|
||||
|
||||
### Phase 1C — Built-in skill and wrapper command
|
||||
|
||||
- Add built-in `skills/live-artifact/SKILL.md`.
|
||||
- Add `od tools live-artifacts ...` and connector command handlers from TypeScript source under `apps/daemon/src`.
|
||||
- Inject daemon URL and short-lived tool token into skill preamble.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Agent can create a live artifact using local data.
|
||||
- UI can list and preview it.
|
||||
- No MCP configuration is required.
|
||||
|
||||
### Phase 2 — Refresh runner
|
||||
|
||||
- Add `refreshes.jsonl` audit log.
|
||||
- Implement manual refresh for local daemon tools.
|
||||
- Implement per-artifact refresh lock, timeout, stale-write protection, and crash recovery.
|
||||
- Preserve previous render on validation failure.
|
||||
- Emit chat-stream UI refresh events and update viewer refresh loading/failure state.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- User can click Refresh and see updated data.
|
||||
- Failed refresh leaves old preview intact and shows actionable error.
|
||||
|
||||
### Phase 3 — Connector catalog and read-only connector tools
|
||||
|
||||
- Port Monet connector catalog/service shape.
|
||||
- Add connector endpoints.
|
||||
- Add `/api/tools/connectors/list` and `/api/tools/connectors/execute`.
|
||||
- Add read-only tool classification.
|
||||
- Add first real read-only connector.
|
||||
- Extend `live-artifact` skill references with connector usage instructions.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Agent can query available connectors.
|
||||
- Agent can create a refreshable artifact from a read-only connector.
|
||||
- Refresh revalidates connector, account, tool, and approval policy.
|
||||
|
||||
### Phase 4 — Optional MCP wrapper
|
||||
|
||||
- Confirmation after the skill + wrapper path: MCP is not needed for MVP correctness because all supported agents can use `SKILL.md` plus `od tools ...` wrappers, and Phase 1C/Phase 3 command surfaces cover live artifact creation, listing, update, refresh, connector listing, and read-only connector execution. MCP is only worth adding as an additive compatibility layer for agents with mature MCP support and must not replace, weaken, or fork the daemon-owned service/policy path.
|
||||
- Wrap the daemon's existing live artifact and connector services as an MCP server for agents that support MCP well.
|
||||
- Do not make MCP required.
|
||||
- Do not mutate global user MCP config automatically.
|
||||
|
||||
#### 11.5.1 Optional MCP server design
|
||||
|
||||
The MCP integration, if added, should be a **thin stdio adapter over the existing daemon tool endpoints**, not a second tool implementation. The MCP process should be launched only for an agent run that explicitly supports MCP and receives the same injected runtime environment as the wrapper CLI:
|
||||
|
||||
```text
|
||||
MCP-capable agent
|
||||
⇄ stdio MCP protocol
|
||||
od mcp live-artifacts # TypeScript source under apps/daemon/src, built into the od bin
|
||||
⇄ local HTTP with Authorization: Bearer $OD_TOOL_TOKEN
|
||||
/api/tools/live-artifacts/* and /api/tools/connectors/*
|
||||
⇄ daemon live artifact, refresh, connector, auth, validation, and policy services
|
||||
```
|
||||
|
||||
Design constraints:
|
||||
|
||||
- **Single policy path:** the MCP server must call the existing `/api/tools/*` endpoints using `OD_DAEMON_URL` and `OD_TOOL_TOKEN`. It must not import store/service modules to bypass token scoping, connector policy, output redaction, rate limits, or route validation.
|
||||
- **Run scoped:** one MCP server instance is scoped to one agent run and one project through the bearer token. It exits when stdio closes; daemon token expiry/revocation remains authoritative.
|
||||
- **Equivalent tools only:** expose MCP tools that mirror the CLI/API surface, with the same schemas and compact results:
|
||||
- `od_live_artifacts_create` → `POST /api/tools/live-artifacts/create`
|
||||
- `od_live_artifacts_list` → `GET /api/tools/live-artifacts/list`
|
||||
- `od_live_artifacts_update` → `POST /api/tools/live-artifacts/update`
|
||||
- `od_live_artifacts_refresh` → `POST /api/tools/live-artifacts/refresh`
|
||||
- `od_connectors_list` → `GET /api/tools/connectors/list`
|
||||
- `od_connectors_execute` → `POST /api/tools/connectors/execute`
|
||||
- **No project overrides:** tool input schemas must not accept `projectId`; project/run scope is always derived from `OD_TOOL_TOKEN` by daemon routes.
|
||||
- **No global config mutation:** OD may display or generate an ephemeral MCP launch descriptor for compatible agents, but must not edit user-level MCP config files automatically.
|
||||
- **No primary-path dependency:** `SKILL.md`, `od tools ...`, and raw-token debugging remain unchanged and continue to work when MCP is disabled or unsupported.
|
||||
- **Typed implementation:** project-owned MCP code should be TypeScript source under `apps/daemon/src` (for example `apps/daemon/src/mcp/live-artifacts-server.ts` plus small CLI dispatch in `apps/daemon/src/cli.ts`). Any JavaScript entrypoint must be generated build output or an explicitly documented compatibility artifact.
|
||||
|
||||
MCP tool errors should translate daemon `ApiErrorResponse` values into MCP tool errors without expanding secret-bearing details. Validation field details may be included only when they are already safe to return from the corresponding `/api/tools/*` route.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Claude Code or another MCP-capable agent can discover equivalent tools through MCP.
|
||||
- Skill + CLI path still works unchanged.
|
||||
|
||||
## 12. File-level landing plan
|
||||
|
||||
Likely new files:
|
||||
|
||||
```text
|
||||
packages/contracts/src/api/live-artifacts.ts
|
||||
packages/contracts/src/api/connectors.ts
|
||||
packages/contracts/src/examples.ts
|
||||
apps/daemon/src/live-artifacts/schema.ts
|
||||
apps/daemon/src/live-artifacts/store.ts
|
||||
apps/daemon/src/live-artifacts/render.ts
|
||||
apps/daemon/src/live-artifacts/refresh.ts
|
||||
apps/daemon/src/live-artifacts/routes.ts
|
||||
apps/daemon/src/connectors/catalog.ts
|
||||
apps/daemon/src/connectors/service.ts
|
||||
apps/daemon/src/connectors/routes.ts
|
||||
apps/daemon/src/tools/live-artifacts.ts
|
||||
apps/daemon/src/tools/connectors.ts
|
||||
skills/live-artifact/SKILL.md
|
||||
skills/live-artifact/references/artifact-schema.md
|
||||
skills/live-artifact/references/connector-policy.md
|
||||
skills/live-artifact/references/refresh-contract.md
|
||||
```
|
||||
|
||||
Likely touched files:
|
||||
|
||||
```text
|
||||
packages/contracts/src/errors.ts
|
||||
packages/contracts/src/index.ts
|
||||
packages/contracts/src/sse/chat.ts
|
||||
apps/daemon/src/server.ts
|
||||
apps/daemon/src/skills.ts
|
||||
apps/daemon/src/cli.ts
|
||||
apps/web/src/providers/daemon.ts
|
||||
apps/web/src/providers/registry.ts
|
||||
apps/web/src/components/EntryView.tsx
|
||||
apps/web/src/components/NewProjectPanel.tsx
|
||||
apps/web/src/components/ProjectView.tsx
|
||||
apps/web/src/components/DesignsTab.tsx
|
||||
apps/web/src/components/FileWorkspace.tsx
|
||||
apps/web/src/components/FileViewer.tsx
|
||||
apps/web/src/components/PreviewModal.tsx
|
||||
apps/web/src/types.ts
|
||||
apps/web/src/prompts/system.ts
|
||||
```
|
||||
|
||||
Keep the first implementation small: current daemon route handlers live in `apps/daemon/src/server.ts`, so either mount live artifact routes there first or add small TypeScript route modules that are imported by `server.ts`. Do not add project-owned `.js` source files; JavaScript should only be generated build output or an explicitly documented compatibility artifact.
|
||||
|
||||
## 13. Security and trust model
|
||||
|
||||
### 13.1 Daemon must enforce
|
||||
|
||||
- `/api/tools/*` requires a short-lived bearer `OD_TOOL_TOKEN`.
|
||||
- Tool tokens are minted per agent run and bind `runId`, `projectId`, allowed endpoints, allowed operations, and expiry.
|
||||
- `/api/tools/*` derives project/run scope from the token and rejects request-supplied project overrides.
|
||||
- CORS for local daemon tool endpoints is closed by default; UI endpoints use the web app's normal origin/session checks.
|
||||
- Defend against CSRF and DNS rebinding on localhost endpoints.
|
||||
- Project ID exists and maps to the active workspace.
|
||||
- All file paths stay inside the project workspace.
|
||||
- Tool output size is bounded.
|
||||
- Snapshot/history size and retention are bounded.
|
||||
- Refresh execution has timeout and cancellation.
|
||||
- Connector credentials never reach agent output or artifact files.
|
||||
- Source input is minimized and redacted.
|
||||
- Read-only refreshes cannot drift into write-capable tools.
|
||||
- Preview responses use iframe sandboxing and restrictive CSP.
|
||||
|
||||
### 13.2 Skill must instruct
|
||||
|
||||
- Do not paste raw connector responses into `artifact.json`.
|
||||
- Do not store tokens, headers, cookies, or credentials.
|
||||
- Prefer summaries, normalized rows, and derived metrics.
|
||||
- Keep `data.json` compact and preview-oriented.
|
||||
- Use daemon tool endpoints for registration and refresh metadata.
|
||||
- Use wrapper commands rather than constructing raw HTTP unless debugging.
|
||||
|
||||
### 13.3 UI must communicate
|
||||
|
||||
- Which connector/account backs the artifact.
|
||||
- When it was last refreshed.
|
||||
- Whether refresh is manual only.
|
||||
- Why a refresh failed.
|
||||
|
||||
## 14. Non-goals
|
||||
|
||||
- No MCP-first implementation in MVP.
|
||||
- No arbitrary write-capable connector refresh.
|
||||
- No raw provider response storage.
|
||||
- No multi-user auth model.
|
||||
- No cloud-hosted connector broker in v1.
|
||||
- No new canvas abstraction separate from OD's existing artifact/preview model.
|
||||
|
||||
## 15. Open questions
|
||||
|
||||
1. Should `od tools ...` be the only wrapper surface, or should generated per-project wrappers also be provided for easier agent access?
|
||||
2. How should agent adapters advertise `shell` availability for skill gating?
|
||||
3. How much refresh history should be retained before compaction?
|
||||
4. Should failed refresh attempt payloads be retained in a hidden failed snapshot directory, or only summarized in `refreshes.jsonl`?
|
||||
|
||||
## 16. Acceptance criteria
|
||||
|
||||
- A built-in `live-artifact` skill can be discovered by the existing skill registry.
|
||||
- An agent can create a live artifact without MCP.
|
||||
- The daemon validates and persists live artifact metadata.
|
||||
- The UI can list and preview the artifact.
|
||||
- Manual refresh works for at least one local read-only source.
|
||||
- Refresh failures are audited and do not destroy the last valid preview.
|
||||
- Connector-backed refresh is read-only and revalidated before every run.
|
||||
- `/api/tools/*` calls require run-scoped auth and cannot override project scope.
|
||||
- No persisted artifact fixture contains raw credentials, headers, cookies, or full provider payloads.
|
||||
|
||||
## 17. Recommended first slice
|
||||
|
||||
Implement the smallest useful vertical slice:
|
||||
|
||||
1. `skills/live-artifact/SKILL.md`
|
||||
2. `packages/contracts/src/api/live-artifacts.ts`
|
||||
3. `apps/daemon/src/live-artifacts/schema.ts`
|
||||
4. `apps/daemon/src/live-artifacts/store.ts`
|
||||
5. `POST /api/tools/live-artifacts/create`
|
||||
6. `GET /api/live-artifacts?projectId=...`
|
||||
7. `GET /api/live-artifacts/:artifactId/preview`
|
||||
8. UI list + virtual live-artifact node + sandboxed preview
|
||||
|
||||
This proves the skill-based interface and storage model before adding connectors, OAuth, refresh runner complexity, or MCP wrappers.
|
||||
340
specs/change/20260430-implement-maintainability-w2-w3/spec.md
Normal file
340
specs/change/20260430-implement-maintainability-w2-w3/spec.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
id: 20260430-implement-maintainability-w2-w3
|
||||
name: Implement Maintainability W2 W3
|
||||
status: implemented
|
||||
created: '2026-04-30'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### Problem Statement
|
||||
|
||||
`apps/web` and `apps/daemon` need the next maintainability-roadmap workstreams implemented: W2 and W3 from `specs/current/maintainability-roadmap.md`. This spec targets a full project migration to TypeScript as the end state; the implementation plan can still use incremental steps so each step is easy to validate.
|
||||
|
||||
### Goals
|
||||
|
||||
- Implement W2: define shared API, SSE, and error contracts covering R2, R7, and R8.
|
||||
- Implement W3 as a full TypeScript migration for project-owned JavaScript entrypoints, modules, scripts, tests, and reporters, covering R1 and related maintainability risk across the repository.
|
||||
- Provide shared request/response types, an SSE event union, and an error model that can be imported by both web and daemon code.
|
||||
- Configure TypeScript so all migrated project code is checked consistently, with migration steps ordered for safe verification.
|
||||
|
||||
### Scope
|
||||
|
||||
- Create or update the shared contract layer for web/daemon request, response, error, and SSE event types.
|
||||
- Add TypeScript configuration and package/script integration needed to migrate daemon, repository scripts, and test support code to TypeScript.
|
||||
- Keep the existing architecture boundary from the roadmap: `apps/web` remains the Next.js frontend and thin BFF/proxy layer; `apps/daemon` remains the local runtime/backend.
|
||||
|
||||
### Constraints
|
||||
|
||||
- W2 depends on the completed W1 ownership and capability boundaries.
|
||||
- W3 should build on W2 for highest-value shared types.
|
||||
- Runtime validation, server modularization, process/task manager work, and broader daemon test pyramid work belong to later roadmap workstreams.
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Web and daemon can import the same contract types.
|
||||
- Project-owned source, scripts, tests, and reporters have a TypeScript migration path with a typed end state.
|
||||
- Typecheck covers the shared contracts, web, daemon, scripts, and test support code included in this spec.
|
||||
- The new contracts explicitly cover HTTP payloads, SSE events, and the unified error model.
|
||||
|
||||
## Research
|
||||
|
||||
### Existing System
|
||||
|
||||
- W2 covers the implicit web/daemon API contract, inconsistent error handling, and under-specified SSE protocol; the roadmap's W3 text names daemon TypeScript support as the original output. Source: `specs/current/maintainability-roadmap.md:57-58`
|
||||
- W1 defines the shared boundary as pure JavaScript or TypeScript usable by both web and daemon, with API DTO types, runtime schemas, task states, SSE event names, and error codes as allowed shared contents. Source: `specs/current/architecture-boundaries.md:41-56`
|
||||
- `apps/web` communicates with daemon-owned capabilities through API DTOs and streaming events, while privileged local filesystem, SQLite, agent CLI, task lifecycle, logs, and artifacts stay daemon-owned. Source: `specs/current/architecture-boundaries.md:13-40`
|
||||
- The workspace currently has no `packages/*` workspace entries; `pnpm-workspace.yaml` includes `apps/*` and `e2e` only. Source: `pnpm-workspace.yaml:1-3`
|
||||
- No shared package currently exists under `packages/*`. Source: file search `packages/*/package.json`
|
||||
- Root scripts run daemon, web, build, tests, and typecheck through pnpm filters; root `typecheck` currently targets only `@open-design/web`. Source: `package.json:12-25`
|
||||
- Dev-mode web rewrites `/api/*`, `/artifacts/*`, and `/frames/*` to the local daemon origin; the config notes that `/api/chat` SSE streams through the rewrite. Source: `apps/web/next.config.ts:35-44`
|
||||
- Web-side daemon chat types live in `apps/web/src/providers/daemon.ts`: `DaemonStreamOptions` sends `agentId`, `history`, `systemPrompt`, `projectId`, `attachments`, `model`, and `reasoning`. Source: `apps/web/src/providers/daemon.ts:19-38`
|
||||
- The web chat client posts `/api/chat` with JSON fields `agentId`, `systemPrompt`, `message`, `projectId`, `attachments`, `model`, and `reasoning`. Source: `apps/web/src/providers/daemon.ts:57-77`
|
||||
- The daemon `/api/chat` handler reads the same request fields from `req.body`, validates agent and message ad hoc, and returns HTTP 400 JSON errors for invalid agent, missing binary, or missing message. Source: `apps/daemon/src/server.ts:868-884`
|
||||
- Web-side `AgentEvent` currently models UI events as `status`, `text`, `thinking`, `tool_use`, `tool_result`, `usage`, and `raw`. Source: `apps/web/src/types.ts:32-39`
|
||||
- Daemon SSE setup for `/api/chat` writes `text/event-stream` frames with `event: <name>` and JSON `data`, using events such as `start`, `agent`, `stdout`, `stderr`, `error`, and `end`. Source: `apps/daemon/src/server.ts:1035-1044`, `apps/daemon/src/server.ts:1087-1095`, `apps/daemon/src/server.ts:1136-1180`
|
||||
- The web SSE parser consumes frame separators, parses event/data fields, maps `stdout` to text, buffers `stderr`, translates `agent` payloads, handles `start`, treats `error` as terminal, and reads `end` exit code. Source: `apps/web/src/providers/daemon.ts:85-151`
|
||||
- Web translation accepts daemon `agent` payload types `status`, `text_delta`, `thinking_delta`, `thinking_start`, `tool_use`, `tool_result`, `usage`, and `raw`; unknown payloads are ignored. Source: `apps/web/src/providers/daemon.ts:178-228`
|
||||
- Agent JSON event parsing emits normalized events such as `status`, `text_delta`, `tool_use`, `tool_result`, `usage`, and `raw`; OpenCode error payloads currently become a `raw` event with embedded error text. Source: `apps/daemon/src/json-event-stream.ts:35-91`
|
||||
- The daemon API proxy has a separate SSE endpoint at `/api/proxy/stream` with request fields `baseUrl`, `apiKey`, `model`, `systemPrompt`, and `messages`, and returns `start`, `delta`, `error`, and `end` SSE events. Source: `apps/daemon/src/server.ts:1188-1192`, `apps/daemon/src/server.ts:1241-1250`, `apps/daemon/src/server.ts:1262-1275`, `apps/daemon/src/server.ts:1291-1303`
|
||||
- HTTP error responses are ad hoc: project routes often return `{ error: string }`, upload errors return `{ code, error }`, and preview errors derive status plus `{ error }`. Source: `apps/daemon/src/server.ts:200-205`, `apps/daemon/src/server.ts:147-177`, `apps/daemon/src/server.ts:755-763`
|
||||
- Project CRUD and conversation/message routes shape common response envelopes such as `{ projects }`, `{ project, conversationId }`, `{ project }`, `{ conversations }`, `{ conversation }`, and `{ messages }`. Source: `apps/daemon/src/server.ts:200-269`, `apps/daemon/src/server.ts:325-424`
|
||||
- File routes shape common response envelopes such as `{ files }`, `{ file }`, and `{ ok: true }`, while raw file routes return binary data. Source: `apps/daemon/src/server.ts:725-752`, `apps/daemon/src/server.ts:776-833`, `apps/daemon/src/server.ts:840-864`
|
||||
- `apps/daemon/src/projects.ts` owns project file DTO construction with fields `name`, `path`, `type`, `size`, `mtime`, `kind`, `mime`, `artifactKind`, and `artifactManifest`. Source: `apps/daemon/src/projects.ts:30-70`
|
||||
- Web application types already include daemon-adjacent DTOs such as `AgentInfo`, `ProjectFileKind`, `ProjectFile`, `Project`, and chat attachment/message/event types in `apps/web/src/types.ts`. Source: `apps/web/src/types.ts:41-101`, `apps/web/src/types.ts:150-160`
|
||||
- `apps/web` has TypeScript configured with `strict`, `noUncheckedIndexedAccess`, `allowJs`, `noEmit`, and a `typecheck` script using `tsc -b --noEmit`. Source: `apps/web/tsconfig.json:2-23`, `apps/web/package.json:6-10`
|
||||
- `apps/daemon` is ESM, starts with `node cli.js`, tests with `vitest run -c vitest.config.ts`, and currently has no `typecheck` script. Source: `apps/daemon/package.json:1-23`
|
||||
- The daemon test config is TypeScript and includes `**/*.test.{ts,tsx,js,mjs,cjs}` under a Node test environment. Source: `apps/daemon/vitest.config.ts:1-8`
|
||||
- `apps/daemon` currently contains JavaScript and MJS project code alongside TypeScript test/config files: `cli.js`, `server.js`, `db.js`, `agents.js`, stream parsers, project/design-system helpers, artifact helpers, `json-event-stream.test.mjs`, `artifact-manifest.test.ts`, and `vitest.config.ts`. Source: file search `apps/daemon/**/*.{js,mjs,cjs,ts,tsx}`
|
||||
- `apps/web` source and config files are TypeScript/TSX, including `next.config.ts`, `app/**/*.tsx`, `src/**/*.ts`, `src/**/*.tsx`, and `vitest.config.ts`. Source: file search `apps/web/**/*.{js,mjs,cjs,ts,tsx}`
|
||||
- `e2e` is mixed: Playwright and Vitest config/tests are TypeScript, while runtime/support scripts and reporters include `.mjs` and `.cjs` files. Source: `e2e/package.json:6-12`, `e2e/playwright.config.ts:1-58`, `e2e/vitest.config.ts:1-12`, file search `e2e/**/*.{js,mjs,cjs,ts,tsx}`
|
||||
- Root scripts are currently MJS files: `scripts/resolve-dev-ports.mjs`, `scripts/dev-all.mjs`, and `scripts/sync-design-systems.mjs`. Source: file search `scripts/**/*.{js,mjs,cjs,ts,tsx}`
|
||||
|
||||
### Available Approaches
|
||||
|
||||
- **Add a new shared workspace package**: create a workspace package for contracts and add it to the pnpm workspace so both `apps/web` and `apps/daemon` can import pure shared TypeScript. This matches the roadmap output of a shared contract layer and the W1 shared-boundary rules. Source: `specs/current/maintainability-roadmap.md:57-58`, `specs/current/architecture-boundaries.md:41-56`, `pnpm-workspace.yaml:1-3`
|
||||
- **Keep shared contracts inside an existing app**: moving contract types under `apps/web` would reuse the current location of many UI-adjacent types, but W1 says shared code should contain pure DTOs and avoid framework or environment-specific APIs. Source: `apps/web/src/types.ts:32-101`, `specs/current/architecture-boundaries.md:41-56`
|
||||
- **Start with type-only contracts**: W2 can define request/response, SSE event, and error model types first, while runtime schemas remain in the later W4 workstream. Source: `specs/current/maintainability-roadmap.md:57-60`
|
||||
- **Migrate repository code to a typed end state in phases**: W3 can add TypeScript configs and package scripts first, then convert daemon modules, root scripts, and e2e support files in bounded batches while running typecheck/test verification after each batch. Source: `apps/daemon/package.json:9-13`, `apps/daemon/vitest.config.ts:1-8`, `e2e/package.json:6-12`, file search `apps/daemon/**/*.{js,mjs,cjs,ts,tsx}`, file search `scripts/**/*.{js,mjs,cjs,ts,tsx}`
|
||||
- **Broaden root typecheck**: root `typecheck` currently targets only web, so full project TypeScript verification requires daemon, shared package, scripts, and e2e/support coverage. Source: `package.json:23`, `apps/daemon/package.json:9-13`, `e2e/package.json:6-12`
|
||||
|
||||
### Constraints & Dependencies
|
||||
|
||||
- W2 depends on completed W1 ownership boundaries, and W3 depends on W2 for the highest-value shared types. Source: `specs/current/maintainability-roadmap.md:56-58`
|
||||
- Runtime validation for HTTP inputs, paths, agents, models, uploads, task IDs, and command args is W4 scope, so research for W2/W3 should capture type boundaries without implementing full validation policy yet. Source: `specs/current/maintainability-roadmap.md:59`
|
||||
- Shared code must stay free of Next.js, Express, Node filesystem/process APIs, browser APIs, SQLite, and daemon internals. Source: `specs/current/architecture-boundaries.md:41-56`
|
||||
- API DTOs should prefer workspace-scoped logical or relative paths; machine absolute paths should remain daemon-internal. Source: `specs/current/architecture-boundaries.md:58-64`
|
||||
- The `/api/chat` stream currently includes daemon-internal `cwd` in the `start` SSE event. Source: `apps/daemon/src/server.ts:1087-1095`
|
||||
- Current daemon SSE lifecycle has no heartbeat or version field in emitted events. Source: `apps/daemon/src/server.ts:1035-1044`, `apps/daemon/src/server.ts:1087-1180`
|
||||
- Current error responses and SSE errors do not use a unified model with `code`, `message`, `details`, `retryable`, and `requestId/taskId`. Source: `apps/daemon/src/server.ts:147-177`, `apps/daemon/src/server.ts:200-205`, `apps/daemon/src/server.ts:868-884`, `apps/daemon/src/server.ts:1170-1180`
|
||||
- Daemon package devDependencies currently include `vitest` only; TypeScript and Node/Express type packages are available in web but not daemon. Source: `apps/daemon/package.json:21-23`, `apps/web/package.json:19-24`
|
||||
- Full-project TypeScript migration includes CommonJS/MJS operational edges such as Playwright reporter loading and Node test/script execution. Source: `e2e/playwright.config.ts:22-37`, `e2e/package.json:8-12`, `package.json:14-15`
|
||||
|
||||
### Key References
|
||||
|
||||
- `specs/current/maintainability-roadmap.md:57-58` - W2/W3 outputs and dependency relationship.
|
||||
- `specs/current/architecture-boundaries.md:41-56` - allowed shared contract contents and shared-code restrictions.
|
||||
- `apps/web/next.config.ts:35-44` - dev proxy boundary for web-to-daemon API and SSE.
|
||||
- `apps/web/src/providers/daemon.ts:19-38` - web-side `/api/chat` request options.
|
||||
- `apps/web/src/providers/daemon.ts:85-151` - web-side SSE frame handling.
|
||||
- `apps/web/src/providers/daemon.ts:178-228` - web-side daemon agent event translation.
|
||||
- `apps/web/src/types.ts:32-39` - current UI `AgentEvent` union.
|
||||
- `apps/daemon/src/server.ts:868-884` - daemon `/api/chat` request field handling and ad hoc HTTP errors.
|
||||
- `apps/daemon/src/server.ts:1035-1044` - daemon SSE frame writer.
|
||||
- `apps/daemon/src/server.ts:1087-1180` - daemon `/api/chat` start/agent/stdout/stderr/error/end lifecycle.
|
||||
- `apps/daemon/src/server.ts:1188-1303` - daemon API proxy stream request and SSE events.
|
||||
- `apps/daemon/src/json-event-stream.ts:35-91` - normalized agent JSON event output.
|
||||
- `apps/daemon/package.json:9-23` - daemon scripts and dependencies.
|
||||
- `apps/web/tsconfig.json:2-23` - web TypeScript baseline.
|
||||
- `e2e/package.json:6-12` - e2e test scripts that currently execute TS config plus MJS runtime support.
|
||||
- `pnpm-workspace.yaml:1-3` - current workspace package globs.
|
||||
|
||||
## Design
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Web[apps/web\nUI + thin BFF/proxy]
|
||||
Contracts[packages/contracts\npure TypeScript DTOs\nSSE unions\nerror model]
|
||||
Daemon[apps/daemon\nlocal capability server]
|
||||
Scripts[scripts + e2e\nTypeScript-covered operational code]
|
||||
|
||||
Web -->|imports HTTP/SSE/error types| Contracts
|
||||
Daemon -->|imports HTTP/SSE/error types| Contracts
|
||||
Web -->|/api/* JSON + SSE| Daemon
|
||||
Scripts -->|typechecked execution helpers| Contracts
|
||||
```
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- Decision: Add a new `packages/contracts` workspace package for W2. The package exports pure TypeScript types for daemon HTTP DTOs, SSE event unions, task states, and error codes; this aligns with the shared-boundary allowed contents and the roadmap's shared-contract output. Source: `specs/current/architecture-boundaries.md:41-56`, `specs/current/maintainability-roadmap.md:57-58`, `pnpm-workspace.yaml:1-3`
|
||||
- Decision: Keep `apps/web/src/types.ts` as the UI/application type layer and move only daemon-facing DTOs/events/errors into `packages/contracts`. Web owns UI state and communicates with daemon through API DTOs and streaming events; current UI `AgentEvent` is a presentation union. Source: `specs/current/architecture-boundaries.md:13-27`, `apps/web/src/types.ts:32-39`, `apps/web/src/types.ts:150-179`, `apps/web/src/types.ts:215-252`
|
||||
- Decision: Model the current daemon API before tightening behavior. Start with type contracts for `/api/chat`, `/api/proxy/stream`, project routes, conversation/message routes, file routes, artifacts, health, agents, skills, and design systems, then add runtime schemas in W4. Source: `specs/current/maintainability-roadmap.md:57-60`, `apps/daemon/src/server.ts:200-269`, `apps/daemon/src/server.ts:725-864`, `apps/daemon/src/server.ts:868-884`, `apps/daemon/src/server.ts:1188-1303`
|
||||
- Decision: Define separate transport-level SSE unions and UI-level event unions. `/api/chat` transport events cover `start`, `agent`, `stdout`, `stderr`, `error`, and `end`; normalized agent payloads cover `status`, `text_delta`, `thinking_delta`, `thinking_start`, `tool_use`, `tool_result`, `usage`, and `raw`; web translation remains liberal for forward compatibility. Source: `apps/daemon/src/server.ts:1035-1044`, `apps/daemon/src/server.ts:1087-1180`, `apps/web/src/providers/daemon.ts:85-151`, `apps/web/src/providers/daemon.ts:178-228`, `apps/daemon/src/json-event-stream.ts:35-91`
|
||||
- Decision: Define a versioned SSE contract shape for future W8/W6 compatibility while preserving the existing event names during W2 adoption. Include a protocol version constant and typed event payloads; heartbeat, cancellation, and canonical task lifecycle events remain future extensions. Source: `specs/current/maintainability-roadmap.md:40-41`, `specs/current/maintainability-roadmap.md:57-64`, `apps/daemon/src/server.ts:1035-1044`, `apps/daemon/src/server.ts:1087-1180`
|
||||
- Decision: Introduce a unified `ApiError` and `SseErrorEvent` type with `code`, `message`, `details`, `retryable`, `requestId`, and `taskId`, plus compatibility helpers for existing `{ error }` and `{ code, error }` responses. Current routes return multiple ad hoc shapes; W2 should make the target contract explicit. Source: `specs/current/maintainability-roadmap.md:39-40`, `apps/daemon/src/server.ts:147-177`, `apps/daemon/src/server.ts:200-205`, `apps/daemon/src/server.ts:868-884`, `apps/daemon/src/server.ts:1170-1180`
|
||||
- Decision: Treat machine absolute paths as daemon-internal in public contracts. DTOs should use project-relative or logical paths; the existing `/api/chat` `start` event's `cwd` field should be typed as legacy/internal and removed from web-facing assumptions during adoption. Source: `specs/current/architecture-boundaries.md:58-64`, `apps/daemon/src/server.ts:1087-1095`
|
||||
- Decision: W3's end state is a compiled TypeScript daemon runtime with a transitional `allowJs` phase. The daemon currently runs `node cli.js` and exposes `./cli.js` as its bin, so TypeScript entrypoint migration needs a deliberate build output and script/bin update. Source: `apps/daemon/package.json:6-13`, `package.json:9-24`
|
||||
- Decision: Broaden typechecking from web-only to contracts, daemon, scripts, and e2e support. Root `typecheck` currently filters only `@open-design/web`; daemon has tests but no typecheck script; e2e already uses TypeScript configs and MJS/CJS operational files. Source: `package.json:19-25`, `apps/daemon/package.json:9-23`, `apps/web/tsconfig.json:2-23`, `e2e/package.json:6-12`, `e2e/playwright.config.ts:1-58`
|
||||
- Decision: Migrate JavaScript/MJS/CJS files in dependency order: pure parsers/helpers, project/artifact helpers, DB/agent modules, server/CLI entrypoints, root scripts, then e2e scripts/reporters. This keeps each step verifiable and limits runtime-loader risk around Playwright reporter loading. Source: `apps/daemon/vitest.config.ts:1-8`, `apps/daemon/src/json-event-stream.ts:35-91`, `e2e/playwright.config.ts:22-37`, `e2e/package.json:8-12`, `package.json:14-15`
|
||||
|
||||
### Why this design
|
||||
|
||||
- A dedicated shared package makes the web/daemon boundary explicit while preserving the existing product architecture: web handles UI/proxy behavior and daemon owns local runtime capabilities. Source: `specs/current/architecture-boundaries.md:13-40`
|
||||
- Type-only W2 contracts deliver immediate drift protection and give W4 a stable target for runtime schemas. Source: `specs/current/maintainability-roadmap.md:57-60`
|
||||
- Separating transport events from UI events keeps daemon protocol evolution independent from rendering concerns and preserves the current liberal parser behavior. Source: `apps/web/src/providers/daemon.ts:85-151`, `apps/web/src/providers/daemon.ts:178-228`
|
||||
- A compiled daemon TypeScript target is the safest full migration end state for the package bin and root `od` entrypoint. Source: `apps/daemon/package.json:6-13`, `package.json:9-10`
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. Create `packages/contracts`, add it to the pnpm workspace, and expose typed exports for API DTOs, SSE events, errors, task states, and shared constants.
|
||||
2. Add package-level and root-level TypeScript configuration so contracts typecheck independently and participate in root `pnpm run typecheck`.
|
||||
3. Replace duplicated web daemon-facing types with imports from `packages/contracts`, keeping UI-only state and presentation unions in `apps/web/src/types.ts`.
|
||||
4. Type daemon request handlers, response envelopes, SSE send helpers, and normalized JSON event parsing against the shared contracts while preserving current runtime behavior.
|
||||
5. Introduce compatibility error helpers and adopt the unified error model first in `/api/chat`, upload errors, project/file routes, and proxy stream errors.
|
||||
6. Add daemon TypeScript config and scripts, migrate daemon modules in dependency order, and switch runtime/bin scripts to compiled JavaScript output once `cli.ts` and `server.ts` are converted.
|
||||
7. Convert root scripts and e2e support scripts/reporters with an explicit execution strategy for Node scripts and Playwright reporter loading.
|
||||
8. Broaden root verification to contracts, web, daemon, scripts, and e2e support, then run targeted daemon/web/e2e tests.
|
||||
|
||||
### Test Strategy
|
||||
|
||||
- Contracts: run `pnpm --filter @open-design/contracts typecheck`; add lightweight type-level coverage via exported example payloads or `tsc`-checked fixture files. Source: `specs/current/maintainability-roadmap.md:57-58`
|
||||
- Web adoption: run `pnpm --filter @open-design/web typecheck` and existing web tests after importing shared DTO/SSE/error types. Source: `apps/web/package.json:6-10`, `apps/web/tsconfig.json:2-23`
|
||||
- Daemon adoption: add and run `pnpm --filter @open-design/daemon typecheck`, then `pnpm --filter @open-design/daemon test`; daemon already uses Vitest with TypeScript config. Source: `apps/daemon/package.json:9-23`, `apps/daemon/vitest.config.ts:1-8`
|
||||
- SSE compatibility: add or update parser/translator tests around `/api/chat` `start`, `agent`, `stdout`, `stderr`, `error`, and `end` frames plus normalized agent payloads. Source: `apps/web/src/providers/daemon.ts:85-151`, `apps/daemon/src/json-event-stream.ts:35-91`
|
||||
- Error model compatibility: add daemon route/helper tests for existing `{ error }` and `{ code, error }` inputs mapping into the new `ApiError` shape. Source: `apps/daemon/src/server.ts:147-177`, `apps/daemon/src/server.ts:200-205`, `apps/daemon/src/server.ts:868-884`
|
||||
- Runtime migration: after each TypeScript conversion batch, run daemon tests and root typecheck; after script/e2e migration, run `pnpm --filter @open-design/e2e test` and a Playwright reporter smoke run when feasible. Source: `package.json:19-25`, `e2e/package.json:6-12`, `e2e/playwright.config.ts:22-37`
|
||||
|
||||
### Pseudocode
|
||||
|
||||
Flow:
|
||||
Add shared package
|
||||
Export contract modules
|
||||
api/chat.ts
|
||||
api/projects.ts
|
||||
api/files.ts
|
||||
sse/chat.ts
|
||||
sse/proxy.ts
|
||||
errors.ts
|
||||
Web imports contracts
|
||||
build typed request body
|
||||
parse transport SSE frame
|
||||
translate typed transport event to UI AgentEvent
|
||||
Daemon imports contracts
|
||||
type request body reads
|
||||
type response envelopes
|
||||
type send(event, data)
|
||||
wrap legacy errors into ApiError shape
|
||||
TypeScript migration proceeds by dependency order
|
||||
helpers/parsers
|
||||
DTO builders
|
||||
services/adapters
|
||||
server/CLI
|
||||
scripts/e2e
|
||||
|
||||
### File Structure
|
||||
|
||||
- `packages/contracts/package.json` - new workspace package metadata, exports, and typecheck script.
|
||||
- `packages/contracts/tsconfig.json` - strict declaration-emitting TypeScript config for shared contracts.
|
||||
- `packages/contracts/src/index.ts` - public export surface.
|
||||
- `packages/contracts/src/api/*.ts` - HTTP request/response DTOs and response envelopes.
|
||||
- `packages/contracts/src/sse/*.ts` - chat/proxy SSE event unions and protocol constants.
|
||||
- `packages/contracts/src/errors.ts` - error codes, `ApiError`, `ApiErrorResponse`, and SSE error payload types.
|
||||
- `packages/contracts/src/tasks.ts` - task state/lifecycle constants shared with later W6/W8 work.
|
||||
- `apps/web/src/types.ts` - keep UI/application types; import shared daemon DTOs where applicable.
|
||||
- `apps/web/src/providers/daemon.ts` - consume shared chat request and SSE event types; retain UI translator.
|
||||
- `apps/daemon/tsconfig.json` - new daemon TypeScript config with transitional `allowJs` and strict checking target.
|
||||
- `apps/daemon/package.json` - add `typecheck`, build/runtime scripts, and TypeScript/type dependencies as migration steps require.
|
||||
- `apps/daemon/**/*.ts` - migrated daemon modules, server, CLI, parsers, and helpers.
|
||||
- `scripts/**/*.ts` - migrated root operational scripts.
|
||||
- `e2e/**/*.ts` - migrated e2e support scripts and reporter strategy.
|
||||
- `AGENTS.md` - repository development conventions for future work: shared contracts first for web/daemon boundaries, TypeScript-first implementation, no project-owned JavaScript entrypoints/modules/scripts/tests/reporters after W3.
|
||||
|
||||
### Interfaces / APIs
|
||||
|
||||
- `ChatRequest`: `{ agentId, message, systemPrompt?, projectId?, attachments?, model?, reasoning? }`, matching web post body and daemon handler reads. Source: `apps/web/src/providers/daemon.ts:57-77`, `apps/daemon/src/server.ts:868-884`
|
||||
- `ChatSseEvent`: discriminated union for `start`, `agent`, `stdout`, `stderr`, `error`, and `end`, with `cwd` treated as legacy/internal on `start`. Source: `apps/daemon/src/server.ts:1035-1044`, `apps/daemon/src/server.ts:1087-1180`
|
||||
- `DaemonAgentPayload`: discriminated union for normalized agent payloads emitted inside `agent` events. Source: `apps/web/src/providers/daemon.ts:178-228`, `apps/daemon/src/json-event-stream.ts:35-91`
|
||||
- `ProxyStreamRequest` and `ProxySseEvent`: request fields `baseUrl`, `apiKey`, `model`, `systemPrompt`, and `messages`; events `start`, `delta`, `error`, and `end`. Source: `apps/daemon/src/server.ts:1188-1303`
|
||||
- `ApiError`: `{ code, message, details?, retryable?, requestId?, taskId? }`; `ApiErrorResponse`: `{ error: ApiError }`; compatibility helpers accept legacy string errors during migration. Source: `specs/current/maintainability-roadmap.md:39-40`, `apps/daemon/src/server.ts:147-177`, `apps/daemon/src/server.ts:200-205`
|
||||
- Response envelopes: projects, conversations, messages, files, and file mutation responses should mirror the current daemon JSON shapes and reuse existing web DTO fields. Source: `apps/daemon/src/server.ts:200-269`, `apps/daemon/src/server.ts:325-424`, `apps/daemon/src/server.ts:725-864`, `apps/web/src/types.ts:150-179`, `apps/web/src/types.ts:215-252`
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Existing SSE consumers should continue ignoring unknown `agent` payloads, so new union members can be added safely. Source: `apps/web/src/providers/daemon.ts:178-228`
|
||||
- Malformed or partial SSE frames should preserve current parser tolerance until W4 validation defines stricter behavior. Source: `apps/web/src/providers/daemon.ts:163-176`
|
||||
- `/api/chat` currently emits terminal SSE errors and HTTP 400 JSON errors through different shapes; W2 should type both and allow incremental adoption. Source: `apps/daemon/src/server.ts:868-884`, `apps/daemon/src/server.ts:1170-1180`
|
||||
- Playwright reporter loading currently points to a `.cjs` reporter path; migration needs a compiled JS reporter path or supported TS execution path. Source: `e2e/playwright.config.ts:22-37`
|
||||
- The root `od` bin and daemon package bin currently point at JavaScript entrypoints; conversion to `.ts` requires a compiled output target before script/bin paths change. Source: `package.json:9-10`, `apps/daemon/package.json:6-13`
|
||||
- Shared contracts should stay pure and free of Next, Express, Node filesystem/process APIs, browser APIs, SQLite, and daemon internals. Source: `specs/current/architecture-boundaries.md:41-56`
|
||||
|
||||
## Plan
|
||||
|
||||
- [x] Step 1: Establish shared contracts package
|
||||
- [x] Substep 1.1 Implement: Add `packages/contracts` workspace package, exports, and strict TypeScript config.
|
||||
- [x] Substep 1.2 Implement: Define `ApiError`, error codes, task states, and common response envelope helpers.
|
||||
- [x] Substep 1.3 Implement: Define HTTP DTOs for chat, proxy stream, projects, conversations, messages, files, agents, skills, design systems, artifacts, and health.
|
||||
- [x] Substep 1.4 Implement: Define `/api/chat` and `/api/proxy/stream` SSE event unions and normalized agent payload unions.
|
||||
- [x] Substep 1.5 Verify: Run contracts typecheck and root package graph install/type resolution checks.
|
||||
- [x] Step 2: Adopt contracts in web and daemon boundary code
|
||||
- [x] Substep 2.1 Implement: Import shared chat/proxy/file/project DTOs in web provider and app types while keeping UI-only unions local.
|
||||
- [x] Substep 2.2 Implement: Type daemon response envelopes, chat request body reads, proxy stream request body reads, and SSE send helpers.
|
||||
- [x] Substep 2.3 Implement: Add compatibility error helpers and adopt them in chat, upload, project/file, and proxy stream paths.
|
||||
- [x] Substep 2.4 Verify: Run web typecheck, daemon tests, and targeted SSE/error compatibility tests.
|
||||
- [x] Step 3: Add daemon TypeScript foundation
|
||||
- [x] Substep 3.1 Implement: Add daemon `tsconfig.json`, `typecheck` script, and required TypeScript/Node/Express type dependencies.
|
||||
- [x] Substep 3.2 Implement: Configure transitional `allowJs` checking for current daemon modules.
|
||||
- [x] Substep 3.3 Implement: Update root `typecheck` to include contracts and daemon.
|
||||
- [x] Substep 3.4 Verify: Run daemon typecheck, daemon tests, and root typecheck.
|
||||
- [x] Step 4: Migrate daemon modules to TypeScript
|
||||
- [x] Substep 4.1 Implement: Convert pure parsers/helpers and their tests first.
|
||||
- [x] Substep 4.2 Implement: Convert project/file/artifact helper modules and DTO builders.
|
||||
- [x] Substep 4.3 Implement: Convert DB, agents, runtime adapter, and stream orchestration modules.
|
||||
- [x] Substep 4.4 Implement: Convert `server` and `cli` entrypoints and switch package/runtime bin paths to compiled output.
|
||||
- [x] Substep 4.5 Verify: Run daemon typecheck/tests after each conversion batch and smoke the daemon CLI locally.
|
||||
- [x] Step 5: Migrate scripts and e2e support to TypeScript
|
||||
- [x] Substep 5.1 Implement: Convert root scripts with a documented Node execution strategy.
|
||||
- [x] Substep 5.2 Implement: Convert e2e runtime/support scripts and preserve Playwright reporter loading through compiled output or supported TS loading.
|
||||
- [x] Substep 5.3 Implement: Update root `typecheck` to include scripts and e2e support.
|
||||
- [x] Substep 5.4 Verify: Run root typecheck, repo test suite, e2e tests, and a Playwright reporter smoke check when feasible.
|
||||
- [x] Step 6: Lock in typed end state and future conventions
|
||||
- [x] Substep 6.1 Implement: Add or update root `AGENTS.md` with W2/W3 development conventions: put shared web/daemon contracts in `packages/contracts`, keep UI-only types in web, keep daemon capability logic in daemon, use TypeScript for new project-owned code, and route runtime validation work to the later validation workstream.
|
||||
- [x] Substep 6.2 Implement: Add an automated residual-JavaScript check for project-owned entrypoints, modules, scripts, tests, and reporters, with explicit allowlist entries only for generated, vendored, or compatibility-output files.
|
||||
- [x] Substep 6.3 Verify: Run the residual-JavaScript check and confirm no project-owned `.js`, `.mjs`, or `.cjs` source files remain outside the documented allowlist.
|
||||
- [x] Substep 6.4 Verify: Re-run root typecheck and full test suite after the final convention and residual-file checks are in place.
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional sections — add what's relevant. -->
|
||||
|
||||
### Implementation
|
||||
|
||||
- `pnpm-workspace.yaml` - added `packages/*` so shared packages participate in the workspace graph.
|
||||
- `packages/contracts/package.json` - added `@open-design/contracts` package metadata, source exports, and `typecheck` script.
|
||||
- `packages/contracts/tsconfig.json` - added strict TypeScript configuration for shared contracts.
|
||||
- `packages/contracts/src/common.ts` - added JSON, nullable, and response envelope helper types.
|
||||
- `packages/contracts/src/errors.ts` - added `ApiError`, error codes, compatibility response types, SSE error payloads, and small pure construction helpers.
|
||||
- `packages/contracts/src/tasks.ts` - added shared task state and task status contracts.
|
||||
- `packages/contracts/src/api/*.ts` - added HTTP DTOs for chat, proxy stream, projects, conversations, messages, files, agents, skills, design systems, artifacts, and health.
|
||||
- `packages/contracts/src/sse/*.ts` - added typed SSE event helpers plus `/api/chat` and `/api/proxy/stream` event unions with protocol constants.
|
||||
- `packages/contracts/src/examples.ts` - added tsc-checked example payloads for key contracts.
|
||||
- `packages/contracts/src/index.ts` - added the public export surface.
|
||||
- `apps/web/package.json` and `apps/daemon/package.json` - added workspace dependencies on `@open-design/contracts` for boundary type adoption.
|
||||
- `apps/web/src/types.ts` - re-exported shared chat, registry, project, file, and conversation DTOs while keeping UI/config-only types local.
|
||||
- `apps/web/src/providers/daemon.ts` - typed `/api/chat` request construction, chat SSE frame handling, daemon agent payload translation, and unified SSE error payload reading with shared contracts.
|
||||
- `apps/daemon/src/server.ts` - added JSDoc contract imports, typed project/file response envelopes, typed chat/proxy request body reads, typed SSE send events, and shared-shape compatibility error helpers.
|
||||
- `apps/daemon/src/server.ts` - adopted `ApiErrorResponse`/`SseErrorPayload` shapes for chat, upload, project/file, and proxy stream error paths while preserving runtime behavior.
|
||||
- `apps/web/src/providers/sse.test.ts` - added coverage for unified daemon SSE error payload handling.
|
||||
- `apps/daemon/sse-response.test.mjs` - added coverage for compatibility `ApiErrorResponse` construction.
|
||||
- `apps/daemon/tsconfig.json` - added a strict daemon TypeScript foundation with `allowJs` for the current JavaScript/MJS transition and bundler resolution for workspace contract source imports.
|
||||
- `apps/daemon/package.json` - added a `typecheck` script plus TypeScript, Node, Express, Multer, and better-sqlite3 type dependencies.
|
||||
- `package.json` - broadened root `typecheck` to run contracts, web, and daemon checks through Corepack-pinned pnpm.
|
||||
- `pnpm-lock.yaml` - updated lockfile entries for daemon TypeScript/type dependencies.
|
||||
- `apps/daemon/*.ts` - migrated remaining daemon-owned modules, helpers, server entrypoint, CLI entrypoint, and daemon tests from `.js`/`.mjs` to `.ts` while preserving runtime `.js` ESM import specifiers for compiled output.
|
||||
- `apps/daemon/tsconfig.json` - switched daemon compilation to NodeNext module resolution, disabled JavaScript source inclusion, and added `dist` declaration/source-map emit.
|
||||
- `apps/daemon/package.json` - added daemon `build`, routed daemon/dev/start through compiled `dist/cli.js`, and updated the package bin to compiled output.
|
||||
- `package.json` - updated the root `od` bin to the compiled daemon CLI path.
|
||||
- `scripts/*.ts` - migrated root development and design-system sync scripts from MJS to TypeScript, using Node 24 `--experimental-strip-types` for direct script execution.
|
||||
- `scripts/tsconfig.json` - added strict TypeScript coverage for root operational scripts.
|
||||
- `e2e/scripts/*.ts` - migrated e2e cleanup and live runtime-adapter smoke scripts to TypeScript; the live script now builds and imports the compiled daemon output.
|
||||
- `e2e/reporters/markdown-reporter.ts` and `e2e/cases/report-metadata.ts` - migrated the Playwright markdown reporter and report metadata from CommonJS to TypeScript ESM.
|
||||
- `e2e/tsconfig.json` and `e2e/package.json` - added e2e support typechecking plus Node strip-types execution for support scripts.
|
||||
- `e2e/playwright.config.ts` - updated root script imports and reporter paths to TypeScript files and routed the Playwright webServer command through Corepack-pinned pnpm.
|
||||
- `package.json` - broadened root `typecheck` to cover scripts and e2e support, and routed root pnpm-invoking scripts through Corepack so the pinned pnpm version is used consistently.
|
||||
- `AGENTS.md` - updated project shape, command notes, TypeScript-first conventions, shared contract boundaries, daemon ownership rules, runtime validation scope, and migrated `.ts` file references for future agents.
|
||||
- `scripts/check-residual-js.ts` - added an automated residual JavaScript scanner for project-owned `.js`, `.mjs`, and `.cjs` files, with documented output/vendor/generated allowlist prefixes and local scratch/dependency directory skips.
|
||||
- `package.json` - added `check:residual-js` and made root `typecheck` run the residual JavaScript check after package/support typechecks and daemon build output generation.
|
||||
|
||||
### Verification
|
||||
|
||||
- `corepack pnpm install` - passed; workspace graph recognized all 5 projects and updated lockfile state.
|
||||
- `corepack pnpm --filter @open-design/contracts typecheck` - passed.
|
||||
- `corepack pnpm --filter @open-design/web typecheck` - passed as a package graph/type resolution sanity check.
|
||||
- `corepack pnpm typecheck` - attempted; failed because the root script invokes `pnpm` from PATH version 10.28.0 while the repo requires `>=10.33.2 <11`. The Corepack package-level equivalent above passed.
|
||||
- `corepack pnpm install` - passed after adding app dependencies on `@open-design/contracts`; lockfile links web and daemon to the workspace package.
|
||||
- `corepack pnpm --filter @open-design/contracts typecheck` - passed after Step 2 adoption.
|
||||
- `corepack pnpm --filter @open-design/web typecheck` - passed after Step 2 adoption.
|
||||
- `corepack pnpm --filter @open-design/web test -- src/providers/sse.test.ts` - passed; Vitest also ran existing artifact manifest tests in the web package.
|
||||
- `corepack pnpm --filter @open-design/daemon test -- sse-response.test.mjs` - passed; Vitest also ran existing daemon artifact manifest and json event stream tests.
|
||||
- `corepack pnpm install` - passed after adding daemon TypeScript/type dependencies.
|
||||
- `corepack pnpm --filter @open-design/daemon typecheck` - passed after adding the daemon TypeScript foundation.
|
||||
- `corepack pnpm --filter @open-design/daemon test` - passed; all 18 daemon tests passed.
|
||||
- `corepack pnpm typecheck` - passed after root `typecheck` was broadened to contracts, web, and daemon and routed through Corepack-pinned pnpm.
|
||||
- `corepack pnpm --filter @open-design/daemon typecheck` - passed after daemon module conversion.
|
||||
- `corepack pnpm --filter @open-design/daemon build` - passed and emitted compiled daemon output under `apps/daemon/dist`.
|
||||
- `corepack pnpm --filter @open-design/daemon test` - passed after daemon module conversion; all 18 daemon tests passed.
|
||||
- `node apps/daemon/dist/cli.js --help` - passed as a compiled CLI smoke check.
|
||||
- `corepack pnpm typecheck` - passed after daemon module conversion and compiled-bin package updates.
|
||||
- `corepack pnpm --filter @open-design/e2e exec tsc -p ../scripts/tsconfig.json --noEmit` - passed for root TypeScript scripts.
|
||||
- `corepack pnpm --filter @open-design/daemon build` - passed before e2e support typechecking and live-script import validation.
|
||||
- `corepack pnpm --filter @open-design/e2e typecheck` - passed for Playwright config, reporter, report metadata, and e2e support scripts.
|
||||
- `corepack pnpm typecheck` - passed after script and e2e support migration.
|
||||
- `node --experimental-strip-types` import smoke checks for `scripts/resolve-dev-ports.ts` and `e2e/reporters/markdown-reporter.ts` - passed.
|
||||
- `corepack pnpm test` - passed; web 15 tests, daemon 18 tests, and e2e Vitest 9 tests passed.
|
||||
- `corepack pnpm --filter @open-design/e2e test:ui:clean` - passed against the TypeScript cleanup script.
|
||||
- `corepack pnpm --filter @open-design/e2e exec playwright test -c playwright.config.ts --list` - passed as a Playwright config/reporter loading smoke check and listed 15 Chromium UI tests.
|
||||
- `corepack pnpm run check:residual-js` - passed; no project-owned residual `.js`, `.mjs`, or `.cjs` files were found outside the documented allowlist.
|
||||
- `corepack pnpm --filter @open-design/e2e exec tsc -p ../scripts/tsconfig.json --noEmit` - passed after adding the residual JavaScript scanner.
|
||||
- `corepack pnpm typecheck` - passed after Step 6; contracts, web, daemon typecheck, daemon build, scripts typecheck, e2e typecheck, and residual JavaScript check all passed.
|
||||
- `corepack pnpm test` - passed after Step 6; web 15 tests, daemon 18 tests, and e2e Vitest 9 tests passed.
|
||||
115
specs/current/architecture-boundaries.md
Normal file
115
specs/current/architecture-boundaries.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Architecture Boundaries
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the architectural boundaries for the local Open Design app. These boundaries are architectural constraints; some enforcement details can be implemented later through the relevant roadmap workstreams.
|
||||
|
||||
## Product Shape
|
||||
|
||||
Open Design is a local-first application. The near-term Electron version is a shell around the same `apps/web` and `apps/daemon` architecture.
|
||||
|
||||
Electron does not introduce a separate privileged application layer. The web layer and daemon keep the same responsibilities in browser and Electron modes.
|
||||
|
||||
## Web Boundary
|
||||
|
||||
`apps/web` owns UI, presentation state, and thin BFF/proxy behavior.
|
||||
|
||||
`apps/web` must not directly access local privileged capabilities:
|
||||
|
||||
- `.od` state
|
||||
- SQLite storage
|
||||
- workspace filesystem reads or writes
|
||||
- agent CLI processes
|
||||
- task process lifecycle
|
||||
- local logs and artifacts
|
||||
|
||||
The web layer communicates with daemon-owned capabilities through API DTOs and streaming events.
|
||||
|
||||
## Daemon Boundary
|
||||
|
||||
`apps/daemon` is the sole local capability server. It owns privileged local runtime behavior:
|
||||
|
||||
- `.od` state
|
||||
- SQLite storage, schema, migrations, and storage layout
|
||||
- workspace filesystem access
|
||||
- agent CLI invocation
|
||||
- task lifecycle and process cleanup
|
||||
- logs, artifacts, and diagnostic state
|
||||
|
||||
Daemon capabilities should be isolated behind internal modules such as `db`, `fs`, `agents`, `tasks`, `logs`, and `artifacts`.
|
||||
|
||||
## Shared Boundary
|
||||
|
||||
Shared code must be pure JavaScript or TypeScript that can run in both web and daemon contexts.
|
||||
|
||||
Shared code may contain:
|
||||
|
||||
- API DTO types
|
||||
- runtime schemas such as Zod or TypeBox schemas
|
||||
- domain constants
|
||||
- task states
|
||||
- SSE event names
|
||||
- error codes
|
||||
- pure helper functions
|
||||
- path-related logical string helpers
|
||||
|
||||
Shared code must not depend on framework or environment-specific APIs such as Next.js, Express, Node filesystem/process APIs, browser-only APIs, SQLite, or daemon internals.
|
||||
|
||||
## API DTO Boundary
|
||||
|
||||
The web layer should understand API DTOs, not daemon implementation details.
|
||||
|
||||
API DTOs should prefer workspace-scoped logical or relative paths. Machine absolute paths should remain daemon-internal. Enforcement can be implemented later through a workspace path resolver and runtime validation layer.
|
||||
|
||||
SQLite schema names, table structure, migration details, and storage layout are daemon-private. The web layer sees API DTOs for display and interaction.
|
||||
|
||||
## Workspace Boundary
|
||||
|
||||
The current architecture can assume one active workspace. Workspace root selection should come from explicit user choice or an explicit startup parameter.
|
||||
|
||||
Daemon filesystem access should be scoped to the active workspace root. Path normalization and root containment checks should be implemented in the daemon path resolver and validation layer.
|
||||
|
||||
Precise implementation priority for workspace enforcement can be deferred, but the boundary direction is fixed: web does not construct privileged filesystem paths, and daemon owns path resolution.
|
||||
|
||||
## Agent Command Boundary
|
||||
|
||||
Users cannot provide free-form shell commands for daemon execution.
|
||||
|
||||
Agent invocations should use controlled command templates and argument construction. User-provided content may enter prompts, files, or configuration fields, while command structure remains daemon-controlled.
|
||||
|
||||
Plugin or custom-agent command extension is outside the current scope.
|
||||
|
||||
## Security Baseline
|
||||
|
||||
The app is local-first. Daemon should bind locally, and local API authentication can be deferred.
|
||||
|
||||
Daemon output should redact sensitive values by default, including tokens, API keys, environment secrets, and Authorization-like headers.
|
||||
|
||||
## Task Lifecycle Boundary
|
||||
|
||||
Daemon owns the full task lifecycle. The web layer may create, subscribe to, query, and request cancellation for tasks through API DTOs and events.
|
||||
|
||||
Tasks belong to a workspace and an agent. Terminal states are:
|
||||
|
||||
- `succeeded`
|
||||
- `failed`
|
||||
- `cancelled`
|
||||
- `interrupted`
|
||||
|
||||
The web layer requests cancellation; daemon determines final task state and owns cleanup. Detailed concurrency, timeout, scheduling, and recovery policies can be defined in the process manager workstream.
|
||||
|
||||
## Deferred Policy Details
|
||||
|
||||
The following policy details can be finalized in later workstreams:
|
||||
|
||||
- multiple workspace support
|
||||
- workspace registry location
|
||||
- artifact, cache, and log directory layout
|
||||
- Electron workspace picker behavior
|
||||
- task concurrency limits
|
||||
- timeout defaults
|
||||
- queueing strategy
|
||||
- restart recovery behavior
|
||||
- process-tree cleanup strategy
|
||||
|
||||
These deferred choices should preserve the boundaries in this document.
|
||||
2240
specs/current/critique-theater-plan.md
Normal file
2240
specs/current/critique-theater-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
546
specs/current/critique-theater.md
Normal file
546
specs/current/critique-theater.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# Critique Theater
|
||||
|
||||
## Naming
|
||||
|
||||
- **Internal codename (engineering, code, prompts, env vars, modules, SSE event prefix, telemetry):** `Critique Theater`. Used in `apps/daemon/src/critique/`, `apps/web/src/components/Theater/`, `OD_CRITIQUE_*`, `critique.*` SSE events.
|
||||
- **User-facing label (UI, settings, docs, README, marketing copy):** `Design Jury`. The string is sourced from a single i18n key `critiqueTheater.userFacingName` so the label can be swapped without touching code.
|
||||
|
||||
The split is deliberate. Engineers reason about the system; users hire a jury.
|
||||
|
||||
## Purpose
|
||||
|
||||
Critique Theater turns Open Design's single-pass artifact generation into a panel-tempered, scored, replayable process. Every artifact is born through a visible five-person Design Jury (Designer, Critic, Brand, A11y, Copy) running inside one CLI session, with auto-converging rounds bounded by a configurable score threshold. By default, no artifact ships under 8.0/10.
|
||||
|
||||
This spec is normative for the v1 implementation and protocol. It is the source of truth that the prompt template, the daemon parser, the SSE event schema, the SQLite columns, the Theater UI components, and the adapter conformance suite all derive from.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Not a new skill protocol. Existing skills under `skills/` work unchanged.
|
||||
- Not a new agent runtime. The daemon spawns the same single CLI per artifact it spawns today.
|
||||
- Not a parallel-process architecture. There is exactly one CLI invocation per artifact lifecycle.
|
||||
- Not a new transport. SSE on the existing project event stream carries every new event variant.
|
||||
- Not a configurable cast in v1. The five panelists are fixed. Cast extension is reserved for v2.
|
||||
|
||||
### Why each non-goal is excluded
|
||||
|
||||
The non-goals above are intentional, not arbitrary. Future readers should know the tradeoff that led to each.
|
||||
|
||||
- **No parallel processes.** A single CLI session keeps the agent's full context coherent: the Designer's draft and every later panelist's notes share one model context, so the Critic can see the actual hierarchy values the Designer chose, the Brand panelist can read the same DESIGN.md the Designer was passed, and the Copy panelist can pick at the verbs the Designer wrote. Splitting this across processes would require a cross-process artifact handoff and a way to replay prior-panelist notes into each one's context, which adds an estimated two to three weeks to the v1 timeline and turns a debugging session into a multi-process trace correlation problem.
|
||||
- **No new agent runtime.** The same on-PATH CLI Open Design already detects (Claude Code, Codex, Cursor Agent, Gemini CLI, etc.) is how this feature reaches users. Building a runtime would replicate work that the existing daemon already does well, would force users onto a model we picked rather than the one they signed up for, and would lose BYOK at every layer.
|
||||
- **No new SSE transport.** SSE is already plumbed through the daemon, the web app, the Electron shell, and the desktop sidecar IPC. A second transport would force every consumer to learn a second connection lifecycle, which is exactly the kind of accidental complexity that takes a feature out of v1.
|
||||
- **No configurable cast in v1.** A fixed five-panelist roster lets the composite formula and weight defaults stay constant across every artifact. A configurable cast adds UX surface (per-skill picker, override storage, settings sync) and requires the score formula to redistribute weight on the fly. We commit to v2 once we have data on which roles users actually drop or add.
|
||||
- **No new skill protocol.** Critique Theater layers on top of the skill loader; it does not change what a skill is. This means the 31 existing skills, the 129 design systems, and any future contributions inherit the panel without per-skill migration work.
|
||||
|
||||
## High-level architecture
|
||||
|
||||
```
|
||||
[ apps/web ] [ apps/daemon ] [ active CLI ]
|
||||
brief form POST brief spawnAgent(skill, brief, cfg) role-plays
|
||||
Theater stage ─────────────────► compose prompt: 5 panelists
|
||||
score badge base skill prompt across up to
|
||||
transcript + design system DESIGN.md 3 rounds in
|
||||
replay + panel.ts protocol addendum one session
|
||||
+ critique config emits XML-ish
|
||||
◄─── SSE events ─────── parse stdout incrementally tagged stream
|
||||
reducer + emit panelEvent / roundEvent
|
||||
selectors / shipEvent / degradedEvent
|
||||
persist transcript ndjson +
|
||||
critique columns in SQLite
|
||||
```
|
||||
|
||||
There are exactly three new modules in the daemon:
|
||||
|
||||
| Module | Responsibility | Inputs | Outputs |
|
||||
| --- | --- | --- | --- |
|
||||
| `apps/daemon/src/critique/parser.ts` | Streaming tokenizer for `<PANELIST>`, `<ROUND_END>`, `<SHIP>` blocks. Handles partial chunks, malformed input, recovery. | `AsyncIterable<string>` (CLI stdout) | `AsyncIterable<PanelEvent>` |
|
||||
| `apps/daemon/src/critique/scoreboard.ts` | Pure state machine. Consumes `PanelEvent`s, buffers per-round, decides ship vs continue using injected config. No I/O. | `PanelEvent`, `CritiqueConfig` | `ScoreboardEvent` (delta-driven) |
|
||||
| `apps/daemon/src/critique/orchestrator.ts` | Wires parser and scoreboard to the SSE bus and SQLite. Owns interrupt cascade, persistence, and degraded fallback. | spawn handle, project id, artifact id | side effects: SSE + DB writes |
|
||||
|
||||
Two new web component groups:
|
||||
|
||||
| Component group | Responsibility |
|
||||
| --- | --- |
|
||||
| `apps/web/src/components/Theater/` | Live theater stage, collapsed score badge, transcript replay, interrupted state, degraded banner, interrupt button. Driven entirely by a pure reducer over the SSE event stream. |
|
||||
| `apps/web/src/prompts/panel.ts` | The protocol addendum injected into every artifact-generating prompt. Exports `PROTOCOL_VERSION`. |
|
||||
|
||||
One new contract package extension:
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `packages/contracts/src/critique.ts` | `CritiqueConfig` zod schema, `PANELIST_ROLES` constants, `PanelEvent` discriminated union, `CritiqueSseEvent` extension to the existing SSE event union. |
|
||||
|
||||
## Configuration
|
||||
|
||||
All thresholds, timeouts, and policies are config-driven. There are no magic numbers in business logic. Defaults live in a single `defaults.ts` next to the schema and are overridable via environment variables read by the existing daemon env layer.
|
||||
|
||||
```ts
|
||||
// packages/contracts/src/critique.ts
|
||||
export const PANELIST_ROLES = ['designer', 'critic', 'brand', 'a11y', 'copy'] as const;
|
||||
export type PanelistRole = typeof PANELIST_ROLES[number];
|
||||
|
||||
export interface CritiqueConfig {
|
||||
enabled: boolean;
|
||||
cast: PanelistRole[];
|
||||
maxRounds: number;
|
||||
scoreThreshold: number;
|
||||
scoreScale: number;
|
||||
weights: Record<PanelistRole, number>;
|
||||
perRoundTimeoutMs: number;
|
||||
totalTimeoutMs: number;
|
||||
parserMaxBlockBytes: number;
|
||||
fallbackPolicy: 'ship_best' | 'ship_last' | 'fail';
|
||||
protocolVersion: number;
|
||||
maxConcurrentRuns: number;
|
||||
}
|
||||
```
|
||||
|
||||
| Env var | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `OD_CRITIQUE_ENABLED` | `false` at M0, `true` from M3 | Master switch. False = legacy generation. |
|
||||
| `OD_CRITIQUE_MAX_ROUNDS` | `3` | Hard upper bound on rounds. |
|
||||
| `OD_CRITIQUE_SCORE_THRESHOLD` | `8.0` | Ship gate. Composite below this continues. |
|
||||
| `OD_CRITIQUE_SCORE_SCALE` | `10` | Score range upper bound. Lower bound is always 0. |
|
||||
| `OD_CRITIQUE_ROUND_TIMEOUT_MS` | `90000` | Per-round wall clock cap. |
|
||||
| `OD_CRITIQUE_TOTAL_TIMEOUT_MS` | `240000` | Total run wall clock cap. |
|
||||
| `OD_CRITIQUE_PARSER_MAX_BLOCK_BYTES` | `262144` | Hard cap on bytes between matched tags. Prevents unbounded buffering. |
|
||||
| `OD_CRITIQUE_FALLBACK_POLICY` | `ship_best` | When threshold never met, which round to keep. |
|
||||
| `OD_CRITIQUE_MAX_CONCURRENT_RUNS` | `os.cpus().length` | Daemon-wide cap. Excess requests queue per project FIFO. |
|
||||
|
||||
The default weights for composite score are `{ designer: 0, critic: 0.40, brand: 0.20, a11y: 0.20, copy: 0.20 }`. Designer is omitted from the composite because Designer drafts; Designer does not score. If a panelist's score is missing in a round, weight redistributes proportionally across present panelists. The weights object is a single source of truth; the formula lives only in `scoreboard.ts`.
|
||||
|
||||
## Wire protocol (`PROTOCOL_VERSION = 1`)
|
||||
|
||||
The format is XML-ish tagged regions, not JSON. Tagged regions tolerate streaming, partial chunks, and model variability where JSON does not. Existing OD prompt files (`directions.ts`, `discovery.ts`) already use this style.
|
||||
|
||||
```
|
||||
<CRITIQUE_RUN version="1" maxRounds="3" threshold="8.0" scale="10">
|
||||
|
||||
<ROUND n="1">
|
||||
<PANELIST role="designer">
|
||||
<NOTES>One sentence stating the design intent for v1.</NOTES>
|
||||
<ARTIFACT mime="text/html"><![CDATA[
|
||||
... self-contained artifact for round 1 ...
|
||||
]]></ARTIFACT>
|
||||
</PANELIST>
|
||||
|
||||
<PANELIST role="critic" score="6.4" must_fix="3">
|
||||
<DIM name="hierarchy" score="6">CTA competes with logo at top-left.</DIM>
|
||||
<DIM name="type" score="7">H1 64px reads as poster, not landing.</DIM>
|
||||
<DIM name="contrast" score="4">CTA 3.9:1, fails AA.</DIM>
|
||||
<DIM name="rhythm" score="6">Vertical gaps 12/16/24/40, no system.</DIM>
|
||||
<DIM name="space" score="5">Hero padding asymmetric L vs R.</DIM>
|
||||
<MUST_FIX>Push CTA background L by 8% to clear AA.</MUST_FIX>
|
||||
<MUST_FIX>Pick a 4px or 8px vertical rhythm.</MUST_FIX>
|
||||
<MUST_FIX>Symmetrize hero horizontal padding.</MUST_FIX>
|
||||
</PANELIST>
|
||||
|
||||
<PANELIST role="brand" score="7.5"> ... </PANELIST>
|
||||
<PANELIST role="a11y" score="5.0"> ... </PANELIST>
|
||||
<PANELIST role="copy" score="6.0"> ... </PANELIST>
|
||||
|
||||
<ROUND_END n="1" composite="6.18" must_fix="7" decision="continue">
|
||||
<REASON>Composite below threshold 8.0; 7 must-fix open.</REASON>
|
||||
</ROUND_END>
|
||||
</ROUND>
|
||||
|
||||
<ROUND n="2"> ... </ROUND>
|
||||
<ROUND n="3"> ... </ROUND>
|
||||
|
||||
<SHIP round="3" composite="8.62" status="shipped">
|
||||
<ARTIFACT mime="text/html"><![CDATA[
|
||||
... final shipped artifact ...
|
||||
]]></ARTIFACT>
|
||||
<SUMMARY>One paragraph describing what changed across rounds and why.</SUMMARY>
|
||||
</SHIP>
|
||||
|
||||
</CRITIQUE_RUN>
|
||||
```
|
||||
|
||||
### Parser invariants
|
||||
|
||||
| Invariant | Enforcement |
|
||||
| --- | --- |
|
||||
| Tags well-formed and balanced | Streaming parser. Malformed input raises `MalformedBlockError`. |
|
||||
| `role` value is in `PANELIST_ROLES` | Unknown role drops the block, emits warning, run continues. |
|
||||
| `score` is `0 <= n <= scoreScale` with one decimal | Out of range clamps to bounds and emits warning. |
|
||||
| `composite` matches recomputed mean within ±0.05 | Mismatch logs warning; daemon's recomputed value wins. |
|
||||
| Each round contains every cast role exactly once | Missing role contributes score `0` for that role; must-fix counter advances. |
|
||||
| Designer round 1 contains exactly one `<ARTIFACT>` | Missing artifact raises `MissingArtifactError` and triggers degraded fallback. |
|
||||
| `<SHIP>` appears exactly once or never | Duplicates: first wins, rest dropped, warning emitted. |
|
||||
| `decision` is `continue` or `ship` | Unknown defaults to `continue` (safe). |
|
||||
| Total bytes between matched open and close tags ≤ `parserMaxBlockBytes` | Excess raises `OversizeBlockError` and triggers degraded fallback. |
|
||||
| CDATA appears only inside `<ARTIFACT>` and `<NOTES>` | Lexer state machine enforces. |
|
||||
|
||||
### Composite score formula
|
||||
|
||||
```ts
|
||||
// Pure function in apps/daemon/src/critique/scoreboard.ts.
|
||||
// weights come from CritiqueConfig.weights, never hardcoded in business logic.
|
||||
const present = roles.filter(r => panelistScore[r] != null);
|
||||
const totalWeight = present.reduce((s, r) => s + weights[r], 0);
|
||||
const composite = present.reduce((s, r) => s + (weights[r] / totalWeight) * panelistScore[r], 0);
|
||||
```
|
||||
|
||||
### Protocol versioning
|
||||
|
||||
- `<CRITIQUE_RUN version="1">` is canonical for v1.
|
||||
- Parser dispatches to `parsers/v1.ts`. Future v2 lands in `parsers/v2.ts` alongside v1; the daemon supports both indefinitely.
|
||||
- Each artifact row stores `critique_protocol_version`. Old artifacts replay through their original parser, never the new one.
|
||||
- The prompt template exports `PROTOCOL_VERSION` as a TypeScript constant; CI fails if this constant is bumped without a new `parsers/v{n}.ts` file.
|
||||
|
||||
### Disagreement requirement
|
||||
|
||||
Every non-final round must contain at least two panelists with diverging `MUST_FIX` directives. The Critic and at least one of {Brand, A11y, Copy} must each emit at least one `MUST_FIX` whose target subsystem differs from the others. The protocol enforces this in the prompt template; the parser emits a `WeakDebate` warning if the round closes without disagreement, which the orchestrator counts toward `parser_errors_total{kind="weak_debate"}` for observability but does not fail the run on.
|
||||
|
||||
### Convergence rule
|
||||
|
||||
A round closes with `decision="ship"` when both:
|
||||
|
||||
- `composite >= scoreThreshold`
|
||||
- Sum of open `must_fix` counts across panelists is `0`
|
||||
|
||||
Otherwise the round closes with `decision="continue"` and the next round begins. After `maxRounds`, the orchestrator applies `fallbackPolicy`:
|
||||
|
||||
- `ship_best`: choose the round with highest composite. Default.
|
||||
- `ship_last`: choose the last completed round.
|
||||
- `fail`: persist the run as `below_threshold` with no shipped artifact; UI shows recovery actions.
|
||||
|
||||
### Self-bound budget
|
||||
|
||||
Round `n+1` transcript bytes must be less than round `n` transcript bytes. Final shipped artifact must be production-ready: no `TODO` comments, no Lorem Ipsum, no broken links. The prompt template enforces; the orchestrator runs a final lint pass before persisting the artifact.
|
||||
|
||||
## SSE event protocol
|
||||
|
||||
The existing `/api/projects/:id/events` SSE stream carries new event variants. All event names live in `packages/contracts/src/sse.ts` as a discriminated union extension, never as string literals in handlers.
|
||||
|
||||
| Event name | Payload fields | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `critique.run_started` | `runId`, `protocolVersion`, `cast`, `maxRounds`, `threshold`, `scale` | Theater handshake. UI initializes lanes from `cast`, lays out rounds from `maxRounds`. |
|
||||
| `critique.panelist_open` | `runId`, `round`, `role` | Tag opened in the stream. UI activates lane caret. |
|
||||
| `critique.panelist_dim` | `runId`, `round`, `role`, `dimName`, `dimScore`, `dimNote` | Per-dim score line. UI animates the corresponding bar. |
|
||||
| `critique.panelist_must_fix` | `runId`, `round`, `role`, `text` | Must-fix directive. UI appends to the lane. |
|
||||
| `critique.panelist_close` | `runId`, `round`, `role`, `score` | Tag closed. UI deactivates caret, locks score. |
|
||||
| `critique.round_end` | `runId`, `round`, `composite`, `mustFix`, `decision`, `reason` | Round closed. UI advances ScoreTicker, RoundDivider increments. |
|
||||
| `critique.ship` | `runId`, `round`, `composite`, `status`, `artifactRef`, `summary` | Run shipped. UI collapses theater into score badge. |
|
||||
| `critique.degraded` | `runId`, `reason`, `adapter` | Parser fallback fired. UI replaces theater with banner. |
|
||||
| `critique.interrupted` | `runId`, `bestRound`, `composite` | User interrupt. UI shows interrupted state. |
|
||||
| `critique.failed` | `runId`, `cause` | Unrecoverable error. UI shows failed state with retry. |
|
||||
| `critique.parser_warning` | `runId`, `kind`, `position` | Non-fatal parser recovery. UI surfaces to log only, never to user, unless `kind == "weak_debate"`. |
|
||||
|
||||
The `artifactRef` payload is a logical reference (`{ projectId, artifactId }`), not the artifact body. The UI fetches the body through the existing artifact endpoint and renders in the existing sandboxed iframe.
|
||||
|
||||
## Persistence
|
||||
|
||||
A new SQLite migration adds critique columns to the existing `artifacts` table. The migration is additive and reversible.
|
||||
|
||||
```sql
|
||||
-- 00XX_critique_rounds.up.sql
|
||||
ALTER TABLE artifacts ADD COLUMN critique_score REAL;
|
||||
ALTER TABLE artifacts ADD COLUMN critique_rounds_json TEXT;
|
||||
ALTER TABLE artifacts ADD COLUMN critique_transcript_path TEXT;
|
||||
ALTER TABLE artifacts ADD COLUMN critique_status TEXT
|
||||
CHECK (critique_status IN ('shipped','below_threshold','timed_out','interrupted','degraded','failed','legacy'));
|
||||
ALTER TABLE artifacts ADD COLUMN critique_protocol_version INTEGER;
|
||||
CREATE INDEX IF NOT EXISTS idx_artifacts_critique_status ON artifacts(critique_status);
|
||||
```
|
||||
|
||||
```sql
|
||||
-- 00XX_critique_rounds.down.sql
|
||||
DROP INDEX IF EXISTS idx_artifacts_critique_status;
|
||||
ALTER TABLE artifacts DROP COLUMN critique_protocol_version;
|
||||
ALTER TABLE artifacts DROP COLUMN critique_status;
|
||||
ALTER TABLE artifacts DROP COLUMN critique_transcript_path;
|
||||
ALTER TABLE artifacts DROP COLUMN critique_rounds_json;
|
||||
ALTER TABLE artifacts DROP COLUMN critique_score;
|
||||
```
|
||||
|
||||
| Column | Format | Notes |
|
||||
| --- | --- | --- |
|
||||
| `critique_score` | `REAL` | Final composite. `NULL` for legacy artifacts. |
|
||||
| `critique_rounds_json` | `TEXT` | Compact summary per round: `[{n, composite, mustFix, decision}]`. Bounded; full transcript on disk. |
|
||||
| `critique_transcript_path` | `TEXT` | Relative path under `.od/artifacts/<artifactId>/`. The stored value is the relative path; absolute resolution is daemon-side only. |
|
||||
| `critique_status` | `TEXT` | Constrained by `CHECK` clause. `legacy` marks artifacts produced before the feature shipped. |
|
||||
| `critique_protocol_version` | `INTEGER` | Pin which `parsers/v{n}.ts` to use for replay. |
|
||||
|
||||
Transcripts are written to `.od/artifacts/<artifactId>/transcript.ndjson` (one `PanelEvent` per line). Files larger than 256 KiB are gzipped to `transcript.ndjson.gz`. The orchestrator chooses the format at write time; the replay path detects the format by extension.
|
||||
|
||||
## UI surface
|
||||
|
||||
All Theater components live under `apps/web/src/components/Theater/`. Each file is under 200 lines.
|
||||
|
||||
```
|
||||
Theater/
|
||||
index.ts
|
||||
TheaterStage.tsx
|
||||
PanelistLane.tsx
|
||||
ScoreTicker.tsx
|
||||
RoundDivider.tsx
|
||||
TheaterCollapsed.tsx
|
||||
TheaterTranscript.tsx
|
||||
TheaterDegraded.tsx
|
||||
InterruptButton.tsx
|
||||
hooks/
|
||||
useCritiqueStream.ts
|
||||
useCritiqueReplay.ts
|
||||
state/
|
||||
reducer.ts
|
||||
selectors.ts
|
||||
__tests__/
|
||||
reducer.test.ts
|
||||
TheaterStage.test.tsx
|
||||
```
|
||||
|
||||
### State machine
|
||||
|
||||
```ts
|
||||
type CritiqueState =
|
||||
| { phase: 'idle' }
|
||||
| { phase: 'running'; runId: string; rounds: Round[]; activeRound: number; activePanelist: PanelistRole | null }
|
||||
| { phase: 'shipped'; runId: string; rounds: Round[]; final: ShipPayload }
|
||||
| { phase: 'degraded'; reason: DegradedReason }
|
||||
| { phase: 'interrupted'; runId: string; rounds: Round[]; bestRound: number }
|
||||
| { phase: 'failed'; runId: string; cause: FailedCause };
|
||||
```
|
||||
|
||||
The reducer is pure. Every transition is covered by a vitest case backed by golden-file SSE replays from the adapter conformance suite. No state lives outside the reducer except UI-only ephemera (lane scroll position, badge expanded vs collapsed).
|
||||
|
||||
### Visual specification
|
||||
|
||||
- Lanes stack vertically inside the right rail with `gap: 12px`. At viewport widths under 720 px the rail itself becomes a full-width drawer.
|
||||
- Per-dim scores render as horizontal bars whose width animates from 0 to `(score / scaleMax) * 100%` with `transition: width 600ms cubic-bezier(.2,.8,.2,1)`.
|
||||
- Animation respects `prefers-reduced-motion: reduce`; bars snap to final width and the score-ticker stops easing.
|
||||
- Lane colors come from CSS custom properties keyed by role. The five role inks are themed against the active design system's OKLch token palette. No hex literals in TSX.
|
||||
- Score badge in collapsed mode renders four colored dots labeled `C` `B` `A` `W` (Critic, Brand, A11y, copy/Word) plus the composite number.
|
||||
|
||||
### Lane density (compact-by-default, expand-on-demand)
|
||||
|
||||
The right rail is dense by nature (5 panelists, multiple dims each, must-fix lists, round dividers). To keep it readable for normal users without losing detail for power users, the panel ships three explicit modes, switchable by a segmented control above the lanes.
|
||||
|
||||
| Mode | Default? | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `Smart` | yes | The active panelist (whichever lane is currently streaming, or, post-ship, the panelist with the lowest score) is expanded. All others are collapsed: head + 1-line summary only. |
|
||||
| `Active only` | no | Only the active panelist is rendered; others are head-only with no summary, used for the most compact view (e.g., embedded in a small workspace tile). |
|
||||
| `Expand all` | no | Every lane is fully expanded. Power users who want to see every dim and every must-fix at once. Persisted as a per-user preference once chosen explicitly. |
|
||||
|
||||
Lane heads are clickable to toggle that single lane's collapsed state, independent of the segmented control. The segmented control is the bulk operation; the lane head is the per-lane override.
|
||||
|
||||
The collapsed-lane summary is **derived**, not authored: it is the most prominent must-fix on that panelist, falling back to the highest-impact dim note if there are no must-fixes (panelist already happy). Truncation at one line with ellipsis. Selectors are pure and unit-tested.
|
||||
|
||||
### "Why this matters" explainer
|
||||
|
||||
Immediately under the composite score card, a single rule line states the ship contract in plain terms:
|
||||
|
||||
> Ships when composite score ≥ `scoreThreshold` and open must-fix == 0. Otherwise refine, up to `maxRounds` rounds.
|
||||
|
||||
Three rendering variants:
|
||||
|
||||
| Phase | Variant |
|
||||
| --- | --- |
|
||||
| `running` | "Ships when composite score ≥ 8.0 and open must-fix == 0. Otherwise refine, up to 3 rounds." |
|
||||
| `shipped` | "Shipped: composite 8.6 ≥ threshold 8.0 with 0 open must-fix. Consensus N of 5 panelists." |
|
||||
| `interrupted` | "Did not meet ship rule (8.0 + 0 must-fix), but you stopped early. Best-of-N was kept, transcript stays available." |
|
||||
|
||||
Numeric values come from `CritiqueConfig`, never hardcoded. The rule line uses a soft dashed border to read as explanatory chrome, not as actionable UI.
|
||||
|
||||
### Label sizing (production, not mockup)
|
||||
|
||||
| Element | Production size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Composite score (big) | 36px / mono / 700 | the headline number |
|
||||
| Per-panelist score | 14px / mono / 700 | colored to match lane ink |
|
||||
| Dim names | 13px / mono | left column, secondary fg color |
|
||||
| Dim numeric scores | 13px / mono / 600 | right column, primary fg color |
|
||||
| Dim notes (sentence) | 13px / sans / 1.55 line-height | sentence-level explanation |
|
||||
| Must-fix body | 13px / sans / 1.5 line-height | red ink on tinted background |
|
||||
| Lane summary (collapsed) | 12px / sans / 1.4 line-height | derived single line, ellipsis at width |
|
||||
| Lane name | 14px / sans / 600 | role label inside the head |
|
||||
| Role tag | 11px / mono / uppercase | colored badge, fixed-width pill |
|
||||
| Round divider | 11px / mono / uppercase / letter-spacing 0.1em | thin separator |
|
||||
| Ship rule explainer | 12px / mono / 1.55 line-height | dashed-border explanatory block |
|
||||
|
||||
Label sizes ≤ 11px are reserved for tags, badges, and uppercase chrome only. **Body text is never below 12px in production.** This rule is enforced by a CI lint that grep-fails any new `font-size: <= 11px` rule outside the explicit chrome class allowlist (`role-tag`, `dim-dot`, `round-divider`, `meta-pill`).
|
||||
|
||||
### Demo-only chrome (must not ship)
|
||||
|
||||
The visual companion mockup includes affordances that are **not part of the product**:
|
||||
|
||||
- The "demo states" tab strip at the top of the rail.
|
||||
- Any footer pill labeled "demo · click tabs to walk every state".
|
||||
- The state-walking keyboard shortcut.
|
||||
|
||||
These exist purely to let reviewers walk every state in one page. The production Theater renders exactly one phase at a time, driven by the SSE stream. CI fails if any string matching `data-demo` or class `demo-tabs` appears in production bundles. The mockup HTML lives under `.superpowers/brainstorm/` (gitignored) and never ships.
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Each lane has `role="region"` with `aria-labelledby` referencing its title.
|
||||
- A single offscreen status node carries `aria-live="polite"`. It announces only `round_end` and `ship` events, never per-dim.
|
||||
- Keyboard map: `Tab` cycles lanes, `Enter` toggles dim detail, `Esc` triggers Interrupt with confirm, `[` / `]` step through rounds in collapsed and replay mode.
|
||||
- Color is never the only signal: must-fix counts use both color and a numeric badge; dim bars carry both color and width; scores always show as text.
|
||||
- The Theater UI is itself audited by a CI test that pipes its rendered DOM through the same WCAG AA rule set the A11y panelist applies.
|
||||
|
||||
### Performance budget
|
||||
|
||||
| Metric | Budget | Enforcement |
|
||||
| --- | --- | --- |
|
||||
| Stage component bundle | ≤ 18 KiB gzipped | `size-limit` in CI |
|
||||
| Reducer hot path | p99 ≤ 2 ms per event | vitest bench in CI |
|
||||
| First lane visible from first SSE event | ≤ 200 ms | Playwright trace in `e2e/critique-theater.spec.ts` |
|
||||
| Transcript scrub at 1 MiB transcript | 60 fps, no dropped frames | Playwright `Page.metrics` |
|
||||
|
||||
### Interrupt semantics
|
||||
|
||||
Pressing the Interrupt button or `Esc` while `phase === 'running'`:
|
||||
|
||||
1. Reducer transitions optimistically to a transient interrupting state. The button disables and labels itself "Interrupting…".
|
||||
2. Web posts `POST /api/projects/:id/critique/:runId/interrupt`.
|
||||
3. Daemon cascades `SIGTERM` to the spawned CLI. Orchestrator drains the parser, flushes the scoreboard, applies `fallbackPolicy` to the rounds completed so far, persists, and emits `critique.interrupted`.
|
||||
4. Reducer advances to `phase: 'interrupted'`. UI shows the best-completed round's artifact plus a tag indicating which round shipped and the score.
|
||||
|
||||
If interrupt fires before any round closes, daemon ships nothing and persists the artifact row as `interrupted` with no `final`. The UI shows an empty state offering one-click retry with the original brief.
|
||||
|
||||
### Replay
|
||||
|
||||
Reopening an artifact loads the badge from SQLite columns. Clicking expand triggers `useCritiqueReplay`, which streams `transcript.ndjson` (or `.gz`) line by line into the same reducer. The same component code path renders both live and replay. Replay supports 1×, 4×, and instant playback speeds and a click-to-scrub timeline.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Failure | Detection | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Model emits malformed block | parser sees opening tag with no close after `parserMaxBlockBytes` or before close tag | parser raises `MalformedBlockError`, orchestrator falls back to `legacy_generation` mode for this run, emits `critique.degraded` with reason. Artifact still ships through the legacy path. |
|
||||
| Score never crosses threshold | scoreboard reaches `maxRounds` without consensus | apply `fallbackPolicy`. Score badge tagged `below_threshold`. |
|
||||
| Per-round timeout | round wall clock exceeds `perRoundTimeoutMs` | abort current round, scoreboard ships best-so-far, badge tagged `timed_out`. |
|
||||
| Total timeout | total wall clock exceeds `totalTimeoutMs` | `SIGTERM` CLI, ship best-so-far, transcript marked partial. |
|
||||
| User Interrupt | `POST /api/projects/:id/critique/:runId/interrupt` | cascade `SIGTERM`, persist partial state, ship best-so-far if any round closed, otherwise mark `interrupted` with no final. |
|
||||
| CLI process crash | spawn handle exits non-zero before `<SHIP>` | persist partial transcript, mark `failed` with `rounds_json.cause`, emit `critique.failed`, never silently retry. |
|
||||
| Daemon restart mid-run | next boot scans SQLite for rows in state `running` older than `totalTimeoutMs` | mark `interrupted` with `rounds_json.recoveryReason = "daemon_restart"`. Never auto-resume. |
|
||||
| Adapter unsupported | adapter fails conformance test in nightly CI | adapter marked `critique:degraded` in adapter registry with 24h TTL. UI shows the degraded banner once per session per adapter. |
|
||||
|
||||
### Failure-mode rate targets and recovery
|
||||
|
||||
Each failure mode above has an empirical rate target, a deterministic recovery path, and a Prometheus signal so the Phase 12 dashboard and the Phase 11 e2e suite can both pin sane thresholds. Targets are starting values; we tune them with the first 1000 production runs.
|
||||
|
||||
| Failure | Target rate | Recovery | Prometheus signal | Alert threshold |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `malformed_block` | < 0.5% of runs per adapter | emit `critique.degraded`, fall through to legacy single-pass generation. No retry. | `open_design_critique_degraded_total{reason="malformed_block",adapter="..."}` | sustained > 2% over 1h on any single adapter |
|
||||
| `oversize_block` | < 0.1% of runs | same as `malformed_block` plus the parser's position is logged for postmortem | `open_design_critique_degraded_total{reason="oversize_block"}` | any non-zero sustained rate is treated as a model regression and pages |
|
||||
| `missing_artifact` | < 0.2% of runs | degraded fallback, prompt template flagged for review (it should make this impossible) | `open_design_critique_degraded_total{reason="missing_artifact"}` | > 1% over 24h |
|
||||
| Score never crosses threshold | < 5% of runs at M3 default | apply `fallbackPolicy` (default `ship_best`); badge tagged `below_threshold`; user can re-run | `open_design_critique_runs_total{status="below_threshold"}` | > 15% sustained over 24h is a quality regression |
|
||||
| Per-round timeout | < 1% of runs | abort current round, ship best-so-far, badge tagged `timed_out` | `open_design_critique_runs_total{status="timed_out"}` | > 3% over 1h on any adapter |
|
||||
| Total timeout | < 0.5% of runs | `SIGTERM` CLI, ship best-so-far, transcript marked partial | same series, distinguished by lifecycle attribute | shares the per-round timeout alert |
|
||||
| User Interrupt | not an error; signal of latency or unwanted direction | preserve transcript, ship best-so-far if any round closed | `open_design_critique_interrupted_total{adapter="..."}` | > 10% over 24h is a UX problem worth investigating |
|
||||
| CLI process crash | < 0.05% of runs | fail loud with `critique.failed`, no silent retry, daemon process surfaces the cause | `open_design_critique_runs_total{status="failed",cause="cli_exit_nonzero"}` | any non-zero sustained rate pages |
|
||||
| Daemon restart mid-run | unbounded (depends on operator action) | persist `interrupted` with `recoveryReason="daemon_restart"`, never auto-resume | `open_design_critique_runs_total{status="interrupted",cause="daemon_restart"}` | informational, no alert |
|
||||
| Adapter unsupported | adapter-specific; surfaces during conformance | adapter marked `critique:degraded` in registry with 24h TTL; degraded banner once per session | `open_design_critique_degraded_total{reason="adapter_unsupported",adapter="..."}` | one alert per adapter on first hit, suppressed during TTL |
|
||||
|
||||
A run can satisfy multiple labels (a `timed_out` run that also `below_threshold`s, for instance). The `status` label is a single canonical value derived by the orchestrator at run end; downstream histograms attach `cause` and `decision` as separate labels.
|
||||
|
||||
The Phase 11 e2e suite synthesizes each row above against a stub adapter and asserts the recovery actually fires. Phase 12 imports the alert thresholds into the Grafana dashboard JSON committed at `tools/dev/dashboards/critique.json` so an operator never has to guess.
|
||||
|
||||
## Concurrency and scalability
|
||||
|
||||
- Orchestrator instances are per-project. Daemon-wide concurrency cap via `OD_CRITIQUE_MAX_CONCURRENT_RUNS`. Excess requests queue with project-level FIFO.
|
||||
- Streaming backpressure: parser is `AsyncIterable`-based. When the SSE consumer is slow, daemon's bounded mailbox (256 events) applies backpressure to the parser, which applies it to CLI stdout via the existing sidecar transport. No unbounded buffers anywhere.
|
||||
- Transcripts larger than 1 MiB stream to disk only; the SQLite row stores a path, never a blob.
|
||||
- Cross-skill reuse: every existing skill receives the panel for free; no per-skill code change is required.
|
||||
- Adapter neutrality: the panel prompt is plain text with no CLI-specific tokens. Every adapter is tested via the conformance harness.
|
||||
|
||||
## Skills protocol extension
|
||||
|
||||
Skills opt out via a new optional frontmatter field in `SKILL.md`:
|
||||
|
||||
```yaml
|
||||
od:
|
||||
critique:
|
||||
policy: always | on-demand | off
|
||||
```
|
||||
|
||||
Default is `always` once `OD_CRITIQUE_ENABLED` flips global at M3. Per-skill overrides ship in the same PR as M2.
|
||||
|
||||
## Adapter conformance
|
||||
|
||||
For each of the 12 CLI adapters and the BYOK proxy, the conformance suite runs a deterministic mini-brief at `temperature=0` (where supported) and asserts:
|
||||
|
||||
- The parser consumes the stream without `MalformedBlockError`.
|
||||
- All required tags appear in the canonical order.
|
||||
- Composite score matches recomputed mean within ±0.05.
|
||||
- `<SHIP>` artifact body parses as well-formed HTML.
|
||||
|
||||
Adapters that fail are marked `critique:degraded` in the adapter registry. The daemon falls back to legacy generation for that adapter and shows the degraded banner once per session. We never pretend feature parity that does not exist.
|
||||
|
||||
## Observability
|
||||
|
||||
| Metric | Type | Labels | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `open_design_critique_runs_total` | counter | `status`, `adapter`, `skill` | Run volume by terminal state. |
|
||||
| `open_design_critique_rounds_total` | counter | `adapter`, `skill` | Average rounds per artifact. |
|
||||
| `open_design_critique_round_duration_ms` | histogram | `quantile`, `adapter`, `skill`, `round` | Round latency distribution. |
|
||||
| `open_design_critique_composite_score` | histogram | `quantile`, `adapter`, `skill` | Output quality distribution. |
|
||||
| `open_design_critique_must_fix_total` | counter | `panelist`, `dim`, `adapter`, `skill` | Where the panel finds problems most often. |
|
||||
| `open_design_critique_degraded_total` | counter | `reason`, `adapter` | Adapter health proxy. |
|
||||
| `open_design_critique_interrupted_total` | counter | `adapter` | User abandonment signal. |
|
||||
| `open_design_critique_parser_errors_total` | counter | `kind`, `adapter` | Parser robustness. |
|
||||
| `open_design_critique_protocol_version` | gauge | `version` | Active protocol versions in use. |
|
||||
|
||||
Structured logs use the existing daemon logger with namespace `critique`. Required events: `run_started`, `round_closed`, `run_shipped`, `degraded`, `parser_recover`, `run_failed`. OpenTelemetry traces wrap each run with spans `critique.run`, `critique.round.<n>`, `critique.parse_chunk`, `critique.scoreboard_eval`, `critique.persist_round`, `critique.ship.persist`.
|
||||
|
||||
A Grafana dashboard ships in `tools/dev/dashboards/critique.json` with three default views: fleet quality (composite p50/p90/p99 over time per adapter), adapter health (degraded ratio plus parser-error rate), and brief throughput (runs per hour, rounds per run, time-to-ship).
|
||||
|
||||
## Security
|
||||
|
||||
| Surface | Threat | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| `<ARTIFACT>` body | XSS in shipped HTML | Existing sandboxed iframe pattern with `sandbox` and CSP headers. No new surface. |
|
||||
| Transcript on disk | Path traversal via stored path | The SQLite column stores a relative path; the daemon resolves under `.od/artifacts/<artifactId>/`; no user-supplied component reaches the resolver. |
|
||||
| Brand `DESIGN.md` content in prompt | Prompt injection from a malicious system | DESIGN.md is wrapped in a `<BRAND_SOURCE>` block whose framing instructs the agent to treat it as data. Same defence as the existing skill loader. |
|
||||
| Score and must-fix from agent stdout | Log injection (newlines, ANSI) | Parser strips ANSI; logs JSON-encode every value; UI renders as text only. |
|
||||
| Pathological agent output | DoS via unbounded buffers | `parserMaxBlockBytes`, `perRoundTimeoutMs`, `totalTimeoutMs` are bounded and config-driven. Orchestrator enforces hard kill. |
|
||||
| BYOK proxy invocation | SSRF / internal-IP exfil | Existing `/api/proxy/stream` blocks internal IPs at the daemon edge. Unchanged. |
|
||||
|
||||
A separate security review pass runs through the `code-reviewer` agent before merge.
|
||||
|
||||
## Testing
|
||||
|
||||
| Layer | Tool | Gate |
|
||||
| --- | --- | --- |
|
||||
| Pure unit | vitest | 95% line and 100% branch on `critique/parser.ts`, `scoreboard.ts`, `reducer.ts`. |
|
||||
| Golden-file fixtures | vitest with `__fixtures__/critique/v1/*.txt` | Each adapter has at least one happy and two malformed transcripts on disk. |
|
||||
| Component | RTL + jsdom | Every reducer phase rendered at least once. |
|
||||
| Integration | vitest + sqlite memory + http mock | End-to-end happy path plus five failure modes. |
|
||||
| Adapter conformance | nightly e2e against live adapters | Each of 12 CLIs plus BYOK proxy must pass canonical brief. |
|
||||
| Playwright e2e | `e2e/critique-theater.spec.ts` | Theater renders within 200 ms, Esc triggers Interrupt, replay scrub at 60 fps. |
|
||||
| Visual regression | Playwright `toHaveScreenshot()` | Each Theater state captured at 375 / 768 / 1280 viewports. |
|
||||
| A11y self-test | axe-playwright | Theater UI passes WCAG AA. |
|
||||
| Performance | size-limit + vitest bench | Bundle ≤ 18 KiB gz; reducer p99 ≤ 2 ms. |
|
||||
| Dead-code | `ts-prune` scoped to `critique/` and `Theater/` | Zero unreferenced exports. |
|
||||
| i18n | existing duplicate-key check plus new missing-key check | All 6 locales present for every Theater string. |
|
||||
| Coverage walker | new `pnpm check:critique-coverage` | Each `CritiqueConfig` field, `PanelEvent` variant, SSE event, SQLite column, protocol grammar element, and i18n key has at least one production reference and one test. |
|
||||
|
||||
## Rollout
|
||||
|
||||
| Milestone | Scope | Default | Reversibility |
|
||||
| --- | --- | --- | --- |
|
||||
| M0 | Code lands behind `OD_CRITIQUE_ENABLED=false`. All tests run. No user-visible change. | off | flip env var |
|
||||
| M1 | Settings UI toggle "Critique Theater (beta)". Adapter conformance grades published in README. 24h TTL on degraded markings. | off | flip toggle |
|
||||
| M2 | Default-on for skills that benefit most: `magazine-poster`, `saas-landing`, `dashboard`, `finance-report`, `hr-onboarding`, `kanban-board`. Lightweight skills (`weekly-update`, `simple-deck`) remain opt-in. Per-skill `od.critique.policy` introduced in `SKILL.md`. | per-skill | per-skill flag |
|
||||
| M3 | After 14 consecutive days at ≥ 90% adapter conformance across the fleet, flip the global default to true. Opt-out remains per-run and per-skill. | on | env var or per-skill |
|
||||
|
||||
Rollback at any milestone flips the master env var. SQLite columns are additive, never required for reads. Existing artifacts keep their badge; new artifacts go through legacy generation. No data migration is needed in either direction.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
Every item is a CI gate. Missing documentation fails the build.
|
||||
|
||||
- `docs/critique-theater.md`, user-facing how-it-works with screenshots of all five states and an adapter compatibility table.
|
||||
- `docs/spec.md`, adds a "Critique Theater protocol v1" section with the full wire grammar.
|
||||
- `docs/architecture.md`, adds the `apps/daemon/src/critique/` module diagram.
|
||||
- `docs/skills-protocol.md`, adds `od.critique.policy` field documentation and extension points.
|
||||
- `docs/agent-adapters.md`, adds the conformance test contract every adapter must satisfy.
|
||||
- `docs/roadmap.md`, adds future panelist extensions (Perf, i18n, Motion, Cost) as future work, not committed scope.
|
||||
- `apps/daemon/src/critique/AGENTS.md`, module-level guide per the existing `AGENTS.md` convention.
|
||||
- `apps/web/src/components/Theater/AGENTS.md`, same.
|
||||
- `README.md`, single line in the "What you get" table: Critique Theater, every artifact panel-tempered, scored, replayable.
|
||||
- All six locales (DE, JA, KO, zh-CN, zh-TW, EN) gain new strings in the same PR. Existing duplicate-key i18n CI gate covers consistency.
|
||||
|
||||
## Open questions
|
||||
|
||||
None. All foundational decisions are locked in this spec. Future work is reserved for v2 (configurable cast, additional panelist roles, multi-CLI parallel debate, score-driven prompt-stack feedback loop) and is out of v1 scope.
|
||||
79
specs/current/maintainability-roadmap.md
Normal file
79
specs/current/maintainability-roadmap.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Maintainability Roadmap
|
||||
|
||||
## Purpose
|
||||
|
||||
This document captures the maintainability risks in the current `apps/web` + `apps/daemon` architecture and the recommended optimization path.
|
||||
|
||||
The architectural boundary stays unchanged:
|
||||
|
||||
- `apps/web`: Next.js frontend and thin BFF/proxy layer.
|
||||
- `apps/daemon`: local runtime/backend for SQLite, `.od` filesystem state, AI agent CLI processes, and SSE streaming.
|
||||
|
||||
The first-principles maintainability goals are:
|
||||
|
||||
- **Understandability**: engineers can locate behavior quickly and reason about data flow.
|
||||
- **Changeability**: common changes can be made with bounded blast radius.
|
||||
- **Verifiability**: contracts, tests, and types catch regressions early.
|
||||
- **Isolation**: high-risk capabilities are contained behind explicit boundaries.
|
||||
- **Recoverability**: failures produce actionable state, logs, and cleanup behavior.
|
||||
|
||||
## Priority Scale
|
||||
|
||||
| Priority | Meaning |
|
||||
|---|---|
|
||||
| P0 | Blocks safe evolution or creates high-risk runtime/security failure modes. |
|
||||
| P1 | Major maintainability risk that increases regression and debugging cost. |
|
||||
| P2 | Medium-term risk that affects reliability, portability, or architecture clarity. |
|
||||
| P3 | Supporting documentation/process improvement. |
|
||||
|
||||
## Risk List and Optimization Plan
|
||||
|
||||
| ID | Priority | Risk | Evidence | Impact | Optimization Plan |
|
||||
|---|---:|---|---|---|---|
|
||||
| R1 | P0 | Daemon lacks TypeScript type checking. | `apps/daemon` is mostly JavaScript while handling API payloads, SQLite rows, filesystem paths, child processes, and SSE events. | API payloads, DB rows, agent events, and task states can drift silently; refactors are riskier. | Add gradual TypeScript support with `allowJs`; write new daemon modules in `.ts`; first type API payloads, SSE events, task lifecycle, DB rows, and agent definitions. |
|
||||
| R2 | P0 | Web/daemon API contract is implicit. | `apps/web` calls daemon through `/api/*` rewrites; web has TypeScript types, daemon returns manually shaped JSON. | Field mismatches surface at runtime; API evolution is fragile. | Create `packages/api-contract` or an equivalent shared contract layer for request, response, error, and SSE event types. |
|
||||
| R3 | P0 | Runtime validation is incomplete at the daemon boundary. | Daemon requests can trigger local filesystem access, SQLite writes, and `child_process.spawn()`. | Type correctness alone cannot protect against malformed runtime input, path traversal, invalid agent IDs, or unsafe args. | Add schema validation at HTTP boundaries with Zod/TypeBox; centralize validation for workspace paths, task IDs, agent IDs, models, reasoning options, uploaded files, and command arguments. |
|
||||
| R4 | P0 | Local capability security boundary needs explicit rules. | Daemon owns high-permission capabilities: local files, `.od`, project workspaces, agent CLIs, and logs. | Unsafe path handling, broad command execution, token leakage, and unintended workspace access become possible failure modes. | Treat daemon as a capability server: bind to localhost, use workspace/path allowlists, normalize and jail paths, allowlist agent commands, and redact sensitive output. |
|
||||
| R5 | P0 | Agent process lifecycle needs a first-class manager. | `/api/chat` spawns multiple agent runtimes and streams output to the frontend. | Zombie processes, cancellation gaps, orphaned tasks, inconsistent exit handling, and concurrent process conflicts. | Introduce a process/task manager with task state machine, cancellation, timeout, cleanup, exit code capture, signal handling, and concurrency limits. |
|
||||
| R6 | P1 | `server.ts` is too monolithic. | `apps/daemon/src/server.ts` contains many routes plus orchestration, filesystem logic, streaming, uploads, and artifact handling. | Harder to understand, test, and change; unrelated edits share the same file and increase regression risk. | Split into thin routes plus services/adapters: `routes/`, `services/`, `agents/`, `db/`, `fs/`, `streams/`, `artifacts/`. |
|
||||
| R7 | P1 | Error handling is inconsistent. | Handlers commonly use local `try/catch` and return ad hoc JSON errors. | UI receives inconsistent failures; logs lose context; task state can stall after partial failures. | Define a unified error model with `code`, `message`, `details`, `retryable`, and `requestId/taskId`; add centralized Express error middleware and adapter-level error mapping. |
|
||||
| R8 | P1 | SSE protocol is under-specified. | Daemon manually writes `text/event-stream` events for agent output and status. | Frontend parsing is fragile; disconnect, heartbeat, terminal events, and error semantics can drift. | Version the SSE event contract and define canonical events such as `task.started`, `task.output`, `task.error`, `task.completed`, `task.cancelled`, and `heartbeat`. |
|
||||
| R9 | P1 | SQLite schema and migration lifecycle need stronger guarantees. | `apps/daemon/src/db.ts` owns local `better-sqlite3` tables and migrations. | Local user data upgrades can fail unpredictably; schema drift is hard to diagnose and recover. | Add explicit migration table, ordered forward migrations, startup migration checks, schema version logging, backup-before-migrate strategy, and migration tests. |
|
||||
| R10 | P1 | Test coverage is thin around daemon behavior. | Existing daemon tests focus on stream parsing and artifact manifest behavior; HTTP/DB/spawn flows have limited coverage. | Changes are validated by manual testing; regressions in filesystem, SQLite, SSE, or agent mocks can ship. | Build layered tests: shared contract tests, route integration tests, service unit tests, SQLite migration tests, SSE parser tests, and agent mock integration tests. |
|
||||
| R11 | P1 | Logging and observability are insufficient for local runtime debugging. | Agent execution involves long-lived tasks, subprocess output, filesystem state, and frontend SSE consumption. | User issues are hard to reproduce; failures lack correlated context. | Add structured logs with `requestId`, `taskId`, `agentId`, `workspace`, exit code, and duration; separate app logs from agent output; redact secrets. |
|
||||
| R12 | P2 | Configuration, port, and health behavior can become fragile. | Web proxies `/api/*` to daemon; dev startup coordinates Next.js and daemon ports. | Port conflicts, daemon-not-ready states, and mismatched environment variables can break startup or distribution. | Centralize config resolution; expose `/health`; add daemon readiness checks; make port selection and UI fallback deterministic. |
|
||||
| R13 | P2 | Cross-platform behavior is a recurring risk. | Daemon uses filesystem paths, SQLite native bindings, shell/process behavior, and signals. | macOS, Linux, and Windows/WSL can differ in path normalization, quoting, permissions, and process termination. | Use Node path APIs consistently, avoid shell string composition, isolate platform-specific process logic, and add CI coverage for supported platforms. |
|
||||
| R14 | P2 | Framework migration can distract from core maintainability issues. | Current complexity is concentrated in FS/spawn/SSE/SQLite and module boundaries. | A framework rewrite can consume time while preserving the risky domain logic. | Keep Express for now; revisit Fastify only after TS, contracts, validation, tests, and modularization are in place and Express becomes a clear limiter. |
|
||||
| R15 | P2 | Web/daemon boundary can erode over time. | Next.js has BFF capability and daemon has backend capability; future edits may blur ownership. | High-permission local runtime logic may leak into `apps/web`; deployment and security assumptions become unclear. | Document and enforce ownership: web handles UI/BFF/proxy; daemon owns local runtime capabilities; shared code contains contracts and pure logic only. |
|
||||
| R16 | P3 | Operational documentation is incomplete. | Local-first daemon behavior depends on ports, `.od`, agent CLIs, runtime logs, and recovery flows. | Onboarding and support costs rise; troubleshooting relies on oral knowledge. | Document daemon architecture, API/SSE contract, task lifecycle, `.od` data layout, agent dependency checks, and common recovery procedures. |
|
||||
|
||||
## Optimization Dependencies
|
||||
|
||||
The optimization work should proceed in dependency order. Some items can run in parallel once their prerequisites are stable.
|
||||
|
||||
| Workstream | Status | Optimization | Covers | Depends on | Output |
|
||||
|---|---|---|---|---|---|
|
||||
| W1 | Completed | Confirm architecture and capability boundaries | R4, R15 | — | Written ownership rules for web, daemon, shared contracts, and dangerous local capabilities. See `specs/current/architecture-boundaries.md`. |
|
||||
| W2 | Completed | Define API, SSE, and error contracts | R2, R7, R8 | W1 | `packages/contracts` now provides shared request/response types, SSE event unions, and error model helpers consumed by web and daemon. |
|
||||
| W3 | Completed | Migrate project-owned code to TypeScript | R1 | W2 for highest-value shared types | Daemon, root scripts, and e2e support now use TypeScript sources; daemon compiles to `apps/daemon/dist`; residual JS is checked by `pnpm guard`. |
|
||||
| W4 | Planned | Add runtime validation at daemon boundaries | R3, R4 | W2 | Schemas for HTTP requests, paths, agents, models, uploads, task IDs, and command args. |
|
||||
| W5 | Planned | Modularize `server.ts` | R6 | W2, W3, W4 | Thin route handlers plus services/adapters for agents, DB, FS, streams, and artifacts. |
|
||||
| W6 | Planned | Introduce agent process/task manager | R5, R8, R11 | W2, W5 | Task state machine, cancellation, timeout, cleanup, exit handling, and concurrency controls. |
|
||||
| W7 | Planned | Strengthen SQLite migrations | R9 | W5 or a clear DB adapter boundary | Migration table, ordered migrations, startup checks, backup strategy, migration tests. |
|
||||
| W8 | Planned | Build the daemon test pyramid | R10 | W2, W4, W5 | Contract tests, route integration tests, service unit tests, migration tests, SSE tests, and mocked agent-process tests. |
|
||||
| W9 | Planned | Add structured logs and observability | R11 | W2, W6 | Correlated request/task logs, sanitized agent output, durations, exit status, and diagnostic context. |
|
||||
| W10 | Planned | Harden config, port, and readiness behavior | R12 | W1 | Centralized config, `/health`, readiness checks, deterministic port behavior. |
|
||||
| W11 | Planned | Harden cross-platform behavior | R13 | W4, W6, W5 | Platform-specific process handling, path normalization rules, supported-platform CI. |
|
||||
| W12 | Planned | Revisit HTTP framework choice | R14 | W2, W3, W4, W5, W8 | Evidence-based decision on whether Express remains adequate or Fastify provides clear net value. |
|
||||
| W13 | Planned | Complete operational documentation | R16 | W1 through W11 as sections stabilize | Current-state docs, runbooks, troubleshooting guides, and recovery procedures. |
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
```text
|
||||
Phase 1: W1 -> W2 -> W3 -> W4
|
||||
Phase 2: W5 -> W6 -> W7 -> W8
|
||||
Phase 3: W9 -> W10 -> W11 -> W13
|
||||
Phase 4: W12
|
||||
```
|
||||
|
||||
The core principle is to reduce risk before changing framework foundations: establish contracts, types, validation, and module boundaries first; then evaluate whether Express remains the right transport layer.
|
||||
544
specs/current/manual-edit-mode-requirements.md
Normal file
544
specs/current/manual-edit-mode-requirements.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# Open Design Manual Edit Mode Requirements
|
||||
|
||||
## Purpose
|
||||
|
||||
This document records the accepted manual edit-mode model from `apps/edit-mode-demo` so it can be migrated into the main Open Design web app.
|
||||
|
||||
The key product decision is:
|
||||
|
||||
- `Comment AI` is the AI-assisted editing path.
|
||||
- `Edit` is the manual HTML/CSS editing path.
|
||||
- `Tweaks` is the global parameter/token editing path.
|
||||
- `Draw` is a visual annotation path, not direct source editing.
|
||||
|
||||
Manual edit mode must let users modify the rendered page directly while keeping the project source file as the only source of truth.
|
||||
|
||||
## Product Boundary
|
||||
|
||||
### Edit Mode
|
||||
|
||||
Edit mode is a manual editor for the current artifact.
|
||||
|
||||
Users should be able to:
|
||||
|
||||
- Select elements from the live preview canvas.
|
||||
- Select elements from a left-side layer list.
|
||||
- Edit element content.
|
||||
- Edit element style.
|
||||
- Edit element attributes.
|
||||
- Edit selected element HTML.
|
||||
- Edit the full artifact source when necessary.
|
||||
- Apply, cancel, undo, redo, reset, and inspect changes.
|
||||
|
||||
Edit mode must not call an AI agent automatically. Any AI-assisted change belongs to `Comment AI`.
|
||||
|
||||
### Comment AI Mode
|
||||
|
||||
Comment AI mode is the scoped agent-edit path:
|
||||
|
||||
- User selects a preview element.
|
||||
- User writes an instruction.
|
||||
- The comment is attached to chat.
|
||||
- Agent patches source.
|
||||
- User reviews the result.
|
||||
|
||||
This feature must remain separate from manual edit mode.
|
||||
|
||||
### Tweaks Mode
|
||||
|
||||
Tweaks mode is for global or generated parameters:
|
||||
|
||||
- CSS tokens.
|
||||
- Theme parameters.
|
||||
- Density.
|
||||
- Type scale.
|
||||
- Accent colors.
|
||||
|
||||
Tweaks can share the same patch/history infrastructure as edit mode, but the UX should be global, not selected-element specific.
|
||||
|
||||
### Draw Mode
|
||||
|
||||
Draw mode is for annotation and review input. It should not mutate HTML/CSS directly in v1.
|
||||
|
||||
## Layout Requirements
|
||||
|
||||
The accepted layout uses a three-region editor composition.
|
||||
|
||||
### Left Rail
|
||||
|
||||
Purpose: mode switching and layer navigation.
|
||||
|
||||
Required sections:
|
||||
|
||||
- Product/header block.
|
||||
- Mode buttons:
|
||||
- `Preview`
|
||||
- `Edit`
|
||||
- `Comment AI`
|
||||
- `Tweaks`
|
||||
- `Draw`
|
||||
- `Layers` list showing selectable elements from the artifact.
|
||||
|
||||
Layer rows must show:
|
||||
|
||||
- Human-readable label.
|
||||
- Element kind.
|
||||
- Stable id or generated path.
|
||||
- Selected state.
|
||||
|
||||
The layer list should prioritize meaningful elements but may include generated container layers when source elements do not have explicit ids.
|
||||
|
||||
### Center Canvas
|
||||
|
||||
Purpose: live artifact preview.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Render artifact in sandboxed iframe.
|
||||
- Preserve Open Design's existing preview model.
|
||||
- In edit mode, selectable elements show subtle outlines.
|
||||
- Hovered/selectable elements should feel discoverable without overwhelming the artifact.
|
||||
- Center toolbar includes:
|
||||
- active mode/status
|
||||
- source line count or file identity
|
||||
- undo
|
||||
- redo
|
||||
- show/hide source
|
||||
- reset
|
||||
|
||||
The canvas stays primary. The editor should not feel like a code-only page.
|
||||
|
||||
### Right Edit Modal
|
||||
|
||||
Purpose: focused properties editor for the selected element.
|
||||
|
||||
Required parts:
|
||||
|
||||
- Modal header:
|
||||
- small kicker: `Manual editor`
|
||||
- selected element label
|
||||
- element kind badge
|
||||
- Selected element metadata:
|
||||
- tag name
|
||||
- element id/path
|
||||
- class name if present
|
||||
- Tabs:
|
||||
- `Content`
|
||||
- `Style`
|
||||
- `Attributes`
|
||||
- `Html`
|
||||
- `Source`
|
||||
- Pinned action footer:
|
||||
- `Cancel`
|
||||
- tab-specific apply button
|
||||
|
||||
The modal should be visually separated from the canvas and left rail, using a stronger shadow and clear header/footer zones.
|
||||
|
||||
### Changes Panel
|
||||
|
||||
Purpose: lightweight patch history.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Shows patch count.
|
||||
- Shows newest changes first.
|
||||
- Shows readable patch label.
|
||||
- Shows raw patch payload for debugging during development.
|
||||
- In production, raw payload can be folded behind a details control.
|
||||
|
||||
## Selection Model
|
||||
|
||||
The preview iframe is the selection surface, not the state owner.
|
||||
|
||||
Selection bridge requirements:
|
||||
|
||||
- Inject an edit bridge into the iframe `srcDoc`.
|
||||
- Host sends `od-edit-mode` to enable or disable edit mode.
|
||||
- Iframe sends:
|
||||
- `od-edit-targets`
|
||||
- `od-edit-select`
|
||||
- The bridge must discover meaningful body elements.
|
||||
- Explicitly annotated elements are preferred:
|
||||
- `data-od-id`
|
||||
- `data-od-edit`
|
||||
- `data-od-label`
|
||||
- If no explicit id exists, generate a stable DOM-path id such as `path-0-1-2`.
|
||||
|
||||
Supported target kinds:
|
||||
|
||||
- `text`
|
||||
- `link`
|
||||
- `image`
|
||||
- `container`
|
||||
- `token`
|
||||
|
||||
Each target should include:
|
||||
|
||||
- `id`
|
||||
- `kind`
|
||||
- `label`
|
||||
- `tagName`
|
||||
- `className`
|
||||
- `text`
|
||||
- `rect`
|
||||
- `fields`
|
||||
- `attributes`
|
||||
- `styles`
|
||||
- `outerHtml`
|
||||
|
||||
## Editing Capabilities
|
||||
|
||||
### Content Tab
|
||||
|
||||
For text-like elements:
|
||||
|
||||
- Edit text content.
|
||||
- Apply via `set-text`.
|
||||
|
||||
For links/buttons:
|
||||
|
||||
- Edit label.
|
||||
- Edit `href` when applicable.
|
||||
- Apply via `set-link`.
|
||||
|
||||
For images:
|
||||
|
||||
- Edit `src`.
|
||||
- Edit `alt`.
|
||||
- Apply via `set-image`.
|
||||
|
||||
Container text editing is allowed in the prototype, but production should be careful: editing container `textContent` may collapse child structure. For production, prefer content editing only for leaf or mostly-text elements.
|
||||
|
||||
### Style Tab
|
||||
|
||||
Style edits should write inline styles to the selected element in v1.
|
||||
|
||||
Supported fields:
|
||||
|
||||
- text color
|
||||
- background color
|
||||
- font size
|
||||
- font weight
|
||||
- text alignment
|
||||
- padding
|
||||
- margin
|
||||
- border radius
|
||||
- border
|
||||
- width
|
||||
- min height
|
||||
|
||||
Apply via `set-style`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Empty style values remove that inline property.
|
||||
- Non-empty values set the corresponding CSS property.
|
||||
- Browser-normalized CSS serialization is acceptable.
|
||||
- Style controls should be small and scannable.
|
||||
|
||||
Future production improvement:
|
||||
|
||||
- Prefer CSS variable/token editing when the artifact exposes safe tokens.
|
||||
- Offer class/style extraction later, not in v1.
|
||||
|
||||
### Attributes Tab
|
||||
|
||||
Attributes are edited as JSON in the prototype.
|
||||
|
||||
Apply via `set-attributes`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Attribute updates are additive/update-only by default.
|
||||
- Omitted attributes must not be deleted.
|
||||
- Empty string values remove that attribute.
|
||||
- Protected attributes must not be removed or overwritten by ordinary attribute edits:
|
||||
- `data-od-id`
|
||||
- `data-od-edit`
|
||||
- `data-od-label`
|
||||
- runtime-only ids
|
||||
- Invalid attribute names are ignored.
|
||||
|
||||
This rule is important because attribute edits must not accidentally remove style/content changes made earlier.
|
||||
|
||||
### Html Tab
|
||||
|
||||
Allows editing the selected element's `outerHTML`.
|
||||
|
||||
Apply via `set-outer-html`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Replacement HTML must parse to one element.
|
||||
- Preserve existing `data-od-id` when replacement omits it.
|
||||
- Preserve existing `data-od-edit` when replacement omits it.
|
||||
- Do not use this as the default editing path for casual users.
|
||||
- Treat it as an advanced escape hatch.
|
||||
|
||||
### Source Tab
|
||||
|
||||
Allows editing the full artifact source.
|
||||
|
||||
Apply via `set-full-source`.
|
||||
|
||||
Rules:
|
||||
|
||||
- This is an advanced escape hatch.
|
||||
- It should be available when the inspector cannot express the desired edit.
|
||||
- Production should add validation and recovery before writing the file.
|
||||
|
||||
## Patch Model
|
||||
|
||||
The source file is the source of truth.
|
||||
|
||||
Patch types:
|
||||
|
||||
```ts
|
||||
type EditPatch =
|
||||
| { id: string; kind: 'set-text'; value: string }
|
||||
| { id: string; kind: 'set-link'; text: string; href: string }
|
||||
| { id: string; kind: 'set-image'; src: string; alt: string }
|
||||
| { id: string; kind: 'set-token'; token: string; value: string }
|
||||
| { id: string; kind: 'set-style'; styles: Partial<EditableStyles> }
|
||||
| { id: string; kind: 'set-attributes'; attributes: Record<string, string> }
|
||||
| { id: string; kind: 'set-outer-html'; html: string }
|
||||
| { kind: 'set-full-source'; source: string };
|
||||
```
|
||||
|
||||
History entry shape:
|
||||
|
||||
```ts
|
||||
interface EditHistoryEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
patch: EditPatch;
|
||||
beforeSource: string;
|
||||
afterSource: string;
|
||||
createdAt: number;
|
||||
}
|
||||
```
|
||||
|
||||
Undo/redo can initially use full-source snapshots. Later it can be optimized to patch inversion.
|
||||
|
||||
## Source Patching Rules
|
||||
|
||||
Source patching should be handled outside the iframe.
|
||||
|
||||
Patch flow:
|
||||
|
||||
1. User selects target from iframe or layer list.
|
||||
2. Host reads the current source document.
|
||||
3. Host fills inspector draft from source and target metadata.
|
||||
4. User edits fields.
|
||||
5. User applies a patch.
|
||||
6. Patch transforms the source string.
|
||||
7. Host updates source state.
|
||||
8. Preview iframe reloads from updated `srcDoc`.
|
||||
9. History records before/after source.
|
||||
|
||||
Production migration should move this from demo state into project file persistence:
|
||||
|
||||
- Read active project file.
|
||||
- Apply patch.
|
||||
- Save file through existing project file API/provider.
|
||||
- Refresh preview.
|
||||
- Add history entry.
|
||||
|
||||
## Validation Requirements
|
||||
|
||||
Minimum v1 validation:
|
||||
|
||||
- Do not apply patches if target cannot be found.
|
||||
- Reject invalid attributes.
|
||||
- Preserve protected attributes.
|
||||
- Reject `outerHTML` replacement that does not produce one element.
|
||||
- Preserve selected id after HTML replacement where possible.
|
||||
- Keep undo/redo usable after every patch.
|
||||
|
||||
Recommended validation before production:
|
||||
|
||||
- HTML parse check.
|
||||
- Basic artifact lint after full-source or HTML edits.
|
||||
- Detect and warn if editing container text would erase child markup.
|
||||
- Warn when image URL is empty.
|
||||
- Warn when `href` is empty or malformed.
|
||||
|
||||
## Persistence Requirements
|
||||
|
||||
The prototype stores source in React state. Production must persist to the active project file.
|
||||
|
||||
Required production persistence behavior:
|
||||
|
||||
- Patch the active file content.
|
||||
- Save through existing project file write API.
|
||||
- Refresh file list and active tab content.
|
||||
- Refresh preview.
|
||||
- Keep current selection if the selected id still exists.
|
||||
- Clear selection if the target no longer exists.
|
||||
- Record change history in memory initially.
|
||||
|
||||
Optional later:
|
||||
|
||||
- Persist edit history per conversation/project.
|
||||
- Add named snapshots.
|
||||
- Add compare/diff view.
|
||||
|
||||
## Accessibility and Interaction Requirements
|
||||
|
||||
Required:
|
||||
|
||||
- Mode buttons use tab semantics.
|
||||
- Inspector tabs use tab semantics.
|
||||
- Inputs have labels.
|
||||
- Keyboard users can apply/cancel from the inspector.
|
||||
- Undo/redo buttons reflect disabled state.
|
||||
- Selected layer is visually distinct.
|
||||
|
||||
Recommended:
|
||||
|
||||
- Keyboard shortcut for edit mode.
|
||||
- Escape cancels active draft.
|
||||
- Enter or Cmd/Ctrl+Enter applies draft where safe.
|
||||
- Search/filter layers.
|
||||
|
||||
## Visual Design Requirements
|
||||
|
||||
The accepted design direction:
|
||||
|
||||
- Quiet editor UI.
|
||||
- Canvas-first.
|
||||
- Figma-like structure:
|
||||
- left layers
|
||||
- center canvas
|
||||
- right properties modal
|
||||
- Open Design-specific mode rail:
|
||||
- Preview
|
||||
- Edit
|
||||
- Comment AI
|
||||
- Tweaks
|
||||
- Draw
|
||||
- Right modal should feel focused and slightly elevated.
|
||||
- Avoid marketing-page styling.
|
||||
- Avoid decorative gradients or large hero styling.
|
||||
- Keep controls dense but readable.
|
||||
- Use 8px radius or less.
|
||||
|
||||
## Migration Targets
|
||||
|
||||
Prototype files to migrate from:
|
||||
|
||||
- `apps/edit-mode-demo/src/editTypes.ts`
|
||||
- `apps/edit-mode-demo/src/editBridge.ts`
|
||||
- `apps/edit-mode-demo/src/sourcePatches.ts`
|
||||
- `apps/edit-mode-demo/src/EditModeDemo.tsx`
|
||||
- `apps/edit-mode-demo/app/styles.css`
|
||||
|
||||
Likely production destinations:
|
||||
|
||||
- `apps/web/src/edit-mode/types.ts`
|
||||
- `apps/web/src/edit-mode/bridge.ts`
|
||||
- `apps/web/src/edit-mode/sourcePatches.ts`
|
||||
- `apps/web/src/components/EditModePanel.tsx`
|
||||
- `apps/web/src/components/EditLayersPanel.tsx`
|
||||
- `apps/web/src/components/FileViewer.tsx`
|
||||
- `apps/web/src/index.css`
|
||||
|
||||
Existing Open Design integration points:
|
||||
|
||||
- `FileViewer` already owns preview iframe and mode toolbar.
|
||||
- Existing comment mode already injects a preview bridge.
|
||||
- Existing project file providers already read/write project files.
|
||||
- Existing tab state already tracks active files.
|
||||
- Existing artifact preview already rebuilds `srcDoc`.
|
||||
|
||||
## Phased Implementation Plan
|
||||
|
||||
### Phase 1: Source-Backed Manual Edit Infrastructure
|
||||
|
||||
Deliver:
|
||||
|
||||
- Edit target types.
|
||||
- Edit patch types.
|
||||
- Source patch helpers.
|
||||
- Iframe edit bridge.
|
||||
- Target discovery and selection.
|
||||
- Manual edit mode state in `FileViewer`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Selecting a target in preview populates inspector data.
|
||||
- Applying `set-text`, `set-link`, `set-image`, and `set-style` updates the project file.
|
||||
- Preview refreshes after save.
|
||||
|
||||
### Phase 2: UI Migration
|
||||
|
||||
Deliver:
|
||||
|
||||
- Left layers list for active artifact.
|
||||
- Right edit modal.
|
||||
- Inspector tabs.
|
||||
- Action footer.
|
||||
- Changes panel or lightweight edit log.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- The production app matches the accepted demo layout.
|
||||
- `Comment AI` and `Edit` are visually and behaviorally distinct.
|
||||
|
||||
### Phase 3: Advanced Source Controls
|
||||
|
||||
Deliver:
|
||||
|
||||
- Attributes JSON tab.
|
||||
- Selected element HTML tab.
|
||||
- Full source tab.
|
||||
- Basic validation and error display.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Advanced edits are possible without breaking simple edit workflows.
|
||||
- Invalid input does not silently corrupt source.
|
||||
|
||||
### Phase 4: Robustness
|
||||
|
||||
Deliver:
|
||||
|
||||
- Undo/redo.
|
||||
- Keep selection after patch when id survives.
|
||||
- Target missing state.
|
||||
- Artifact lint after risky edits.
|
||||
- Optional diff preview.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Manual edit mode can be used repeatedly on a real generated artifact without losing work.
|
||||
|
||||
## Non-Goals for v1
|
||||
|
||||
- Drag-and-drop layout editing.
|
||||
- Freeform vector editing.
|
||||
- Multi-user collaboration.
|
||||
- Auto-layout system.
|
||||
- Component extraction.
|
||||
- Class-based CSS refactoring.
|
||||
- AI agent calls from manual edit mode.
|
||||
|
||||
These can be designed later after the source-backed manual edit loop is stable.
|
||||
|
||||
## Acceptance Checklist
|
||||
|
||||
- [ ] `Edit` mode does not call AI.
|
||||
- [ ] `Comment AI` remains the only agent-assisted edit path.
|
||||
- [ ] Preview element selection works.
|
||||
- [ ] Layer list selection works.
|
||||
- [ ] Right edit modal shows selected layer identity.
|
||||
- [ ] Content edits work.
|
||||
- [ ] Style edits work.
|
||||
- [ ] Attribute edits work without deleting unrelated attributes.
|
||||
- [ ] Selected HTML edits work.
|
||||
- [ ] Full source edits work.
|
||||
- [ ] Undo/redo works.
|
||||
- [ ] Source view reflects applied changes.
|
||||
- [ ] Preview refreshes after applied changes.
|
||||
- [ ] Invalid patches do not corrupt the artifact.
|
||||
- [ ] UI remains canvas-first and simple.
|
||||
264
specs/current/run.md
Normal file
264
specs/current/run.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Run Model and Recovery Flow
|
||||
|
||||
## Purpose
|
||||
|
||||
A run is one daemon-owned background execution instance for a user request. It lets the daemon keep an agent task alive across web page refreshes, tab closes, route changes, and temporary SSE disconnects.
|
||||
|
||||
The frontend owns presentation state. The daemon owns execution state. SSE owns live subscription and replay.
|
||||
|
||||
## Concept Model
|
||||
|
||||
A project is the top-level design workspace. It contains conversations, owns artifacts, and provides the daemon working directory for agent execution.
|
||||
|
||||
A conversation is a thread inside a project. It contains ordered messages and provides the UI context for multi-turn work.
|
||||
|
||||
A message is user-visible conversation content. A user message records the request. An assistant message records the generated response and can be backed by one run while generation is active or recoverable.
|
||||
|
||||
A run is a daemon-owned execution instance. It belongs to one project and one conversation, and it targets one assistant message. The run starts and supervises one agent process, records execution status, and stores replayable SSE events.
|
||||
|
||||
The intended cardinality is:
|
||||
|
||||
- One project contains many conversations.
|
||||
- One conversation contains many messages.
|
||||
- One project can have many runs.
|
||||
- One conversation can have many runs.
|
||||
- One assistant message can have zero or one run.
|
||||
- One run belongs to one project, one conversation, and one assistant message.
|
||||
- One run can start one agent process during active execution.
|
||||
|
||||
The recovery path follows the user-visible hierarchy: open a project, load a conversation, find assistant messages with active run metadata, then reattach to the daemon run.
|
||||
|
||||
## Concept Responsibilities
|
||||
|
||||
### Project
|
||||
|
||||
A project is the design workspace. It provides:
|
||||
|
||||
- project metadata, such as skill, design system, and fidelity;
|
||||
- the daemon working directory, usually `.od/projects/<projectId>/`;
|
||||
- artifact ownership;
|
||||
- the top-level scope for conversations and runs.
|
||||
|
||||
### Conversation
|
||||
|
||||
A conversation is a thread inside a project. It provides:
|
||||
|
||||
- the ordered message history;
|
||||
- the UI context for multi-turn work;
|
||||
- the grouping key for active run recovery.
|
||||
|
||||
### Message
|
||||
|
||||
A message is user-visible conversation content. An assistant message is also the durable UI container for a run result. It should store:
|
||||
|
||||
- `runId`: the daemon execution backing this assistant response;
|
||||
- `runStatus`: the latest known run state;
|
||||
- `lastRunEventId`: the latest applied SSE event ID;
|
||||
- partial generated content, persisted during streaming.
|
||||
|
||||
### Run
|
||||
|
||||
A run is a daemon-managed execution instance. It provides:
|
||||
|
||||
- agent process startup;
|
||||
- execution status, such as `queued`, `running`, `succeeded`, `failed`, or `canceled`;
|
||||
- replayable SSE events;
|
||||
- reconnect support through `events?after=<lastRunEventId>`;
|
||||
- explicit cancellation through the cancel endpoint.
|
||||
|
||||
Each run should carry `projectId`, `conversationId`, and `assistantMessageId`. These fields let the daemon recover active work for a reopened project page and let the frontend attach output to the correct assistant message.
|
||||
|
||||
## Primary Communication Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Web as Web UI
|
||||
participant API as Web API Proxy
|
||||
participant Daemon as Daemon
|
||||
participant Agent as Agent Process
|
||||
participant Store as Message Store
|
||||
|
||||
Web->>Web: Generate assistantMessageId and clientRequestId
|
||||
Web->>Store: Persist user message
|
||||
Web->>Store: Persist empty assistant message with runStatus=running
|
||||
|
||||
Web->>API: POST /api/runs with projectId, conversationId, assistantMessageId, clientRequestId, request payload
|
||||
API->>Daemon: Forward POST /api/runs
|
||||
Daemon->>Daemon: Create run with status=queued
|
||||
Daemon->>Agent: Spawn agent in project workspace
|
||||
Daemon->>Daemon: Mark run status=running
|
||||
Daemon-->>API: 202 runId
|
||||
API-->>Web: 202 runId
|
||||
|
||||
Web->>Store: Persist runId on assistant message
|
||||
Web->>API: GET run events
|
||||
API->>Daemon: Attach SSE client
|
||||
Daemon-->>Web: SSE start event with id
|
||||
|
||||
loop Streaming output
|
||||
Agent-->>Daemon: stdout / structured event
|
||||
Daemon->>Daemon: Append event to run buffer
|
||||
Daemon-->>Web: SSE stdout / agent event with id
|
||||
Web->>Web: Apply event to assistant message
|
||||
Web->>Store: Throttled persist content and lastRunEventId
|
||||
end
|
||||
|
||||
Agent-->>Daemon: Process close
|
||||
Daemon->>Daemon: Mark terminal status
|
||||
Daemon-->>Web: SSE end event with status
|
||||
Web->>Store: Persist final content, runStatus, and lastRunEventId
|
||||
```
|
||||
|
||||
## Refresh and Reattach Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Web1 as Web UI Before Refresh
|
||||
participant Web2 as Web UI After Refresh
|
||||
participant API as Web API Proxy
|
||||
participant Daemon as Daemon
|
||||
participant Agent as Agent Process
|
||||
participant Store as Message Store
|
||||
|
||||
Web1->>API: GET run events
|
||||
API->>Daemon: Attach SSE client
|
||||
Daemon-->>Web1: SSE events
|
||||
|
||||
Web1-xAPI: Page refresh closes browser subscription
|
||||
Note over Daemon,Agent: Run continues in daemon and agent process keeps running
|
||||
Agent-->>Daemon: More output while page is unavailable
|
||||
Daemon->>Daemon: Buffer events with increasing IDs
|
||||
|
||||
Web2->>Store: Load project conversations and messages
|
||||
Store-->>Web2: Assistant message with runId, runStatus, lastRunEventId
|
||||
Web2->>API: GET run status
|
||||
API->>Daemon: Fetch run status
|
||||
Daemon-->>Web2: Run status
|
||||
|
||||
alt Run is active
|
||||
Web2->>API: GET run events after lastRunEventId
|
||||
API->>Daemon: Reattach SSE client after last applied event
|
||||
Daemon-->>Web2: Replay missed events
|
||||
Daemon-->>Web2: Continue live SSE events
|
||||
Web2->>Store: Persist resumed content and lastRunEventId
|
||||
else Run is terminal
|
||||
Web2->>API: GET run events after lastRunEventId
|
||||
API->>Daemon: Request remaining buffered events
|
||||
Daemon-->>Web2: Replay remaining events and end
|
||||
Web2->>Store: Persist terminal runStatus
|
||||
end
|
||||
```
|
||||
|
||||
## Active Run Fallback Flow
|
||||
|
||||
The frontend should persist `runId` on the assistant message immediately after run creation. A small failure window still exists between daemon run creation and message update. The daemon should also support an active run list endpoint as a recovery fallback.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Web as Web UI
|
||||
participant API as Web API Proxy
|
||||
participant Daemon as Daemon
|
||||
participant Store as Message Store
|
||||
|
||||
Web->>Store: Load messages for project and conversation
|
||||
Store-->>Web: Assistant message with runStatus=running and no runId
|
||||
Web->>API: GET active runs for project and conversation
|
||||
API->>Daemon: Query active runs
|
||||
Daemon-->>Web: Active runs with assistantMessageId
|
||||
Web->>Web: Match run.assistantMessageId to assistant message ID
|
||||
Web->>Store: Persist recovered runId on assistant message
|
||||
Web->>API: GET run events after lastRunEventId
|
||||
API->>Daemon: Reattach SSE client
|
||||
Daemon-->>Web: Replay and live events
|
||||
```
|
||||
|
||||
## Explicit Cancel Flow
|
||||
|
||||
Browser subscription lifetime and daemon run lifetime are separate. Refresh, tab close, and route changes close the local subscription only. The daemon receives a cancel request only when the user explicitly clicks Stop.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Web as Web UI
|
||||
participant API as Web API Proxy
|
||||
participant Daemon as Daemon
|
||||
participant Agent as Agent Process
|
||||
participant Store as Message Store
|
||||
|
||||
Web->>Web: User clicks Stop
|
||||
Web->>API: POST cancel run
|
||||
API->>Daemon: Forward cancel request
|
||||
Daemon->>Daemon: Mark cancelRequested=true
|
||||
Daemon->>Agent: Send SIGTERM
|
||||
Agent-->>Daemon: Process closes
|
||||
Daemon->>Daemon: Mark run status=canceled
|
||||
Daemon-->>Web: SSE end event with status=canceled
|
||||
Web->>Store: Persist runStatus=canceled
|
||||
```
|
||||
|
||||
## API Surface
|
||||
|
||||
Recommended run APIs:
|
||||
|
||||
```http
|
||||
POST /api/runs
|
||||
GET /api/runs/:id
|
||||
GET /api/runs/:id/events?after=<lastRunEventId>
|
||||
GET /api/runs?projectId=<projectId>&conversationId=<conversationId>&status=active
|
||||
POST /api/runs/:id/cancel
|
||||
```
|
||||
|
||||
`POST /api/runs` should accept correlation fields:
|
||||
|
||||
```ts
|
||||
interface ChatRunCreateRequest {
|
||||
projectId: string;
|
||||
conversationId: string;
|
||||
assistantMessageId: string;
|
||||
clientRequestId: string;
|
||||
agentId: string;
|
||||
message: string;
|
||||
model?: string | null;
|
||||
reasoning?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
`GET /api/runs/:id` should return enough state for recovery:
|
||||
|
||||
```ts
|
||||
interface ChatRunStatusResponse {
|
||||
id: string;
|
||||
projectId: string;
|
||||
conversationId: string;
|
||||
assistantMessageId: string;
|
||||
agentId: string;
|
||||
status: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
exitCode?: number | null;
|
||||
signal?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
## Persistence Phases
|
||||
|
||||
### Phase 1: Refresh and Tab Close Survival
|
||||
|
||||
- Keep daemon runs in memory.
|
||||
- Persist `runId`, `runStatus`, `lastRunEventId`, and partial assistant content in the message store.
|
||||
- Reattach after refresh while the daemon process is still alive.
|
||||
- Keep terminal run metadata and event buffers long enough for short-term UI recovery.
|
||||
|
||||
### Phase 2: Daemon Restart Visibility
|
||||
|
||||
- Persist `chat_runs` and `chat_run_events` in daemon storage.
|
||||
- Mark active runs as interrupted after daemon restart because the child process exits with the daemon.
|
||||
- Preserve terminal status and buffered output for user-facing history.
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
- A browser fetch abort should close only the local SSE subscription.
|
||||
- The Stop button is the only UI action that should call `/api/runs/:id/cancel`.
|
||||
- The frontend should persist `runId` immediately after `POST /api/runs` succeeds.
|
||||
- The frontend should process SSE events idempotently using `lastRunEventId`.
|
||||
- The daemon should allow multiple simultaneous SSE clients for one run.
|
||||
- The daemon should expose active runs by project and conversation for fallback recovery.
|
||||
304
specs/current/runtime-adapter.md
Normal file
304
specs/current/runtime-adapter.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Runtime Adapter Current State
|
||||
|
||||
## Purpose
|
||||
|
||||
Runtime Adapter is the daemon layer responsible for adapting local AI agent CLIs. It converts Open Design's unified generation requests into the actual command-line invocations for each CLI, and converts CLI output into streaming events that the frontend can consume.
|
||||
|
||||
The current implementation is concentrated in:
|
||||
|
||||
- `apps/daemon/src/agents.ts`: agent definitions, detection, model lists, argument construction, model validation.
|
||||
- `apps/daemon/src/server.ts`: `/api/chat` request orchestration, prompt composition, `spawn()` subprocesses, SSE forwarding.
|
||||
- `apps/daemon/src/claude-stream.ts`: parsing Claude Code structured JSONL output.
|
||||
- `apps/daemon/src/json-event-stream.ts`: parsing structured JSON/JSONL output from Codex, Gemini, OpenCode, and Cursor Agent.
|
||||
- `apps/daemon/src/acp.ts`: model detection and streaming session orchestration for the ACP JSON-RPC runtime.
|
||||
|
||||
## Currently Supported Runtimes
|
||||
|
||||
`AGENT_DEFS` in `apps/daemon/src/agents.ts` defines 8 local runtimes:
|
||||
|
||||
| id | Name | CLI | Output format | Model list source |
|
||||
|---|---|---|---|---|
|
||||
| `claude` | Claude Code | `claude` | `claude-stream-json` | Static fallback |
|
||||
| `codex` | Codex CLI | `codex` | `json-event-stream` | Static fallback |
|
||||
| `gemini` | Gemini CLI | `gemini` | `json-event-stream` | Static fallback |
|
||||
| `opencode` | OpenCode | `opencode` | `json-event-stream` | `opencode models` + fallback |
|
||||
| `hermes` | Hermes | `hermes` | `acp-json-rpc` | `session/new` from `hermes acp` + fallback |
|
||||
| `kimi` | Kimi CLI | `kimi` | `acp-json-rpc` | `session/new` from `kimi acp` + fallback |
|
||||
| `cursor-agent` | Cursor Agent | `cursor-agent` | `json-event-stream` | `cursor-agent models` + fallback |
|
||||
| `qwen` | Qwen Code | `qwen` | `plain` | Static fallback |
|
||||
|
||||
Each runtime definition contains:
|
||||
|
||||
- `id` / `name` / `bin`: used for frontend display and process startup.
|
||||
- `versionArgs`: used to detect the version.
|
||||
- `fallbackModels`: static fallback options for the model selector.
|
||||
- `listModels`: optional model discovery command.
|
||||
- `fetchModels`: optional custom model detection logic, suitable for runtimes such as ACP that require a handshake before the model list is available.
|
||||
- `reasoningOptions`: optional reasoning effort options, currently used by Codex.
|
||||
- `buildArgs()`: converts unified input into the CLI's argv; it can also read `runtimeContext` at runtime, currently used to explicitly pass execution context such as `cwd`.
|
||||
- `streamFormat`: tells the daemon how to interpret stdout.
|
||||
|
||||
## Detection Flow
|
||||
|
||||
The detection entry point is `detectAgents()`.
|
||||
|
||||
Flow:
|
||||
|
||||
1. Iterate over `AGENT_DEFS`.
|
||||
2. Use `resolveOnPath()` to locate the CLI binary in `PATH`.
|
||||
3. After locating it, run `versionArgs` to get the version.
|
||||
4. Generate the model list through `listModels`, `fetchModels`, or `fallbackModels`, depending on runtime capabilities.
|
||||
5. Return the result to the frontend and refresh the runtime's model validation cache.
|
||||
|
||||
The detection result includes:
|
||||
|
||||
- `available`: whether the CLI is available.
|
||||
- `path`: the actual binary path.
|
||||
- `version`: version string.
|
||||
- `models`: model list used by the frontend model menu.
|
||||
- `reasoningOptions`: reasoning effort menu.
|
||||
- `streamFormat`: output format hint.
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
Actual execution happens in `POST /api/chat` in `apps/daemon/src/server.ts`.
|
||||
|
||||
Flow:
|
||||
|
||||
1. The frontend submits `agentId`, user message, system prompt, project ID, attachments, model, and reasoning options.
|
||||
2. The daemon uses `getAgentDef(agentId)` to find the runtime definition.
|
||||
3. The daemon creates or locates `.od/projects/<projectId>/` as the agent working directory.
|
||||
4. The daemon validates uploaded image paths and project attachment paths.
|
||||
5. The daemon combines the system prompt, working directory hint, existing file list, attachment list, and user request into one prompt.
|
||||
6. The daemon prepares additional readable directories: `skills/` and `design-systems/`.
|
||||
7. The daemon validates the model and reasoning option.
|
||||
8. It calls `def.buildArgs(...)` to generate CLI arguments; currently it also passes `runtimeContext = { cwd }` for CLIs that need an explicit workspace argument.
|
||||
9. It starts the local runtime with `spawn(def.bin, args, { cwd })`; plain / Claude use read-only stdin, and ACP runtimes use writable stdin.
|
||||
10. The daemon forwards runtime output to the frontend through SSE.
|
||||
|
||||
## Output Stream Handling
|
||||
|
||||
There are currently four output formats:
|
||||
|
||||
### Claude Code: Structured JSONL
|
||||
|
||||
Claude Code uses:
|
||||
|
||||
```bash
|
||||
claude -p <prompt> --output-format stream-json --verbose --include-partial-messages
|
||||
```
|
||||
|
||||
The daemon parses stdout through `createClaudeStreamHandler()` and converts Claude Code JSONL events into UI events:
|
||||
|
||||
- `status`
|
||||
- `text_delta`
|
||||
- `thinking_delta`
|
||||
- `thinking_start`
|
||||
- `tool_use`
|
||||
- `tool_result`
|
||||
- `usage`
|
||||
|
||||
These events are sent to the frontend through the SSE `agent` event.
|
||||
|
||||
### Codex / Gemini / OpenCode / Cursor Agent: Structured JSON Event Stream
|
||||
|
||||
These four runtimes currently use the unified `json-event-stream` output format, with stdout parsed by `apps/daemon/src/json-event-stream.ts`.
|
||||
|
||||
#### Codex
|
||||
|
||||
Codex currently uses:
|
||||
|
||||
```bash
|
||||
codex exec --json --skip-git-repo-check --sandbox workspace-write -c sandbox_workspace_write.network_access=true -C <cwd>
|
||||
```
|
||||
|
||||
The current integration uses the lightweight structured path through `exec --json`. Compared with the original plain-text `codex exec`, this path adds:
|
||||
|
||||
- `--json`: structured event output
|
||||
- `--skip-git-repo-check`: allows running in a temporary working directory
|
||||
- `--sandbox workspace-write`: allows Codex to edit within the project workspace without using the deprecated `--full-auto` shortcut
|
||||
- `-c sandbox_workspace_write.network_access=true`: keeps network access enabled inside the workspace-write sandbox
|
||||
- `-C <cwd>`: explicit working directory
|
||||
|
||||
The daemon currently maps:
|
||||
|
||||
- `thread.started` → `status(initializing)`
|
||||
- `turn.started` → `status(running)`
|
||||
- `item.completed(agent_message)` → `text_delta`
|
||||
- `turn.completed.usage` → `usage`
|
||||
|
||||
#### Gemini
|
||||
|
||||
Gemini currently uses:
|
||||
|
||||
```bash
|
||||
GEMINI_CLI_TRUST_WORKSPACE=true gemini --output-format stream-json --yolo
|
||||
```
|
||||
|
||||
The daemon delivers the prompt over stdin rather than argv. It currently maps:
|
||||
|
||||
- `init` → `status(initializing)`
|
||||
- `message(role=assistant)` → `text_delta`
|
||||
- `result.stats` → `usage`
|
||||
|
||||
Gemini may still output some workspace scan warnings on stderr at runtime; the main flow remains unaffected.
|
||||
|
||||
#### OpenCode
|
||||
|
||||
OpenCode currently uses:
|
||||
|
||||
```bash
|
||||
opencode run --format json --dangerously-skip-permissions <prompt>
|
||||
```
|
||||
|
||||
When the user selects a model, `--model <id>` is appended.
|
||||
|
||||
The daemon currently maps:
|
||||
|
||||
- `step_start` → `status(running)`
|
||||
- `text` → `text_delta`
|
||||
- `tool_use` → `tool_use`
|
||||
- Completed `tool_use.state` → `tool_result`
|
||||
- `step_finish.part.tokens` → `usage`
|
||||
|
||||
#### Cursor Agent
|
||||
|
||||
Cursor Agent currently uses:
|
||||
|
||||
```bash
|
||||
cursor-agent --print --output-format stream-json --stream-partial-output --force --trust --workspace <cwd> -p <prompt>
|
||||
```
|
||||
|
||||
When the user selects a model, `--model <id>` is appended.
|
||||
|
||||
The daemon currently maps:
|
||||
|
||||
- `system(subtype=init)` → `status(initializing)`
|
||||
- `assistant` partial chunks with `timestamp_ms` → `text_delta`
|
||||
- `result.usage` → `usage`
|
||||
|
||||
Cursor outputs both partial assistant chunks and the final aggregated assistant message. The daemon currently prioritizes partial chunks and ignores the final aggregated text after partial chunks have appeared, avoiding duplicate rendering.
|
||||
|
||||
#### Qoder
|
||||
|
||||
Qoder currently uses:
|
||||
|
||||
```bash
|
||||
qodercli -p --output-format stream-json --permission-mode bypass_permissions
|
||||
```
|
||||
|
||||
The daemon delivers the composed prompt over stdin rather than argv. When runtime context is available, `--cwd <cwd>` is appended. When the user selects a model, `--model <id>` is appended. Additional readable directories are passed as repeated `--add-dir <dir>` pairs.
|
||||
|
||||
Validated uploaded image paths are passed as repeated `--attachment <path>` pairs so Qoder receives the original multimodal context in addition to the textual `@path` prompt hint.
|
||||
|
||||
The daemon parses Qoder stream-json output through `apps/daemon/src/qoder-stream.ts` and currently maps:
|
||||
|
||||
- `system(subtype=init)` → `status(initializing)`
|
||||
- assistant text content blocks → `text_delta`
|
||||
- thinking content blocks → `thinking_start` / `thinking_delta`
|
||||
- assistant error records → `error`
|
||||
- result usage metadata → `usage`
|
||||
|
||||
### Qwen: Plain Text Pass-through
|
||||
|
||||
Qwen currently still uses the `plain` output format.
|
||||
|
||||
The daemon directly forwards stdout chunks to the frontend through the SSE `stdout` event, and stderr chunks through the `stderr` event.
|
||||
|
||||
### Hermes / Kimi: ACP JSON-RPC
|
||||
|
||||
Hermes uses:
|
||||
|
||||
```bash
|
||||
hermes acp --accept-hooks
|
||||
```
|
||||
|
||||
Kimi uses:
|
||||
|
||||
```bash
|
||||
kimi acp
|
||||
```
|
||||
|
||||
The daemon starts an ACP session over stdio through `apps/daemon/src/acp.ts`:
|
||||
|
||||
1. `initialize`
|
||||
2. `session/new`
|
||||
3. Optional `session/set_model`
|
||||
4. `session/prompt`
|
||||
|
||||
When an ACP runtime actively emits `session/request_permission`, the daemon prefers `approve_for_session`, which supports headless automatic approval for CLIs such as Kimi that require approval before tool calls.
|
||||
|
||||
The `session/new` response returns `sessionId`, `models.availableModels`, and `models.currentModelId`. The daemon reuses this information for model detection and runtime status reporting.
|
||||
|
||||
It then converts Hermes / Kimi `session/update` events into frontend-consumable `agent` events:
|
||||
|
||||
- `agent_thought_chunk` → `thinking_start` / `thinking_delta`
|
||||
- `agent_message_chunk` → `text_delta`
|
||||
- Final usage from `session/prompt` → `usage`
|
||||
|
||||
At runtime, two additional status events are added:
|
||||
|
||||
- Emit `status(model)` after `session/new` returns the default model.
|
||||
- Emit `status(streaming)` when the first text token arrives, including `ttftMs`.
|
||||
|
||||
Model detection also reuses ACP: during detection, the daemon reads `models.availableModels` and `models.currentModelId` from the `session/new` response.
|
||||
|
||||
The current Kimi MVP integration directly reuses the Hermes ACP orchestrator. Automatic permission approval has been added to the shared ACP layer. `multica` also contains Kimi-specific tool title normalization and provider error sniffing; this repository currently keeps a lighter implementation.
|
||||
|
||||
## Prompt Injection Approach
|
||||
|
||||
Local CLIs currently use a unified approach of folding the system prompt into the user message.
|
||||
|
||||
The reason is that most local code-agent CLI command-line entry points lack an independent system channel. The daemon composes the following content into a single input:
|
||||
|
||||
- `systemPrompt`: base output contract + skill content + design system content.
|
||||
- `cwdHint`: current working directory and file writing rules.
|
||||
- `filesListBlock`: existing file list in the project directory.
|
||||
- `attachmentHint`: attachments uploaded or selected by the user.
|
||||
- `message`: original user request.
|
||||
- `safeImages`: temporary uploaded image paths appended in `@path` form.
|
||||
|
||||
Claude Code additionally exposes `skills/` and `design-systems/` through `--add-dir`, making it easier for the agent to read skill seeds, templates, and design system files.
|
||||
|
||||
## Safety and Validation
|
||||
|
||||
Existing protections include:
|
||||
|
||||
- Process startup uses `spawn()` argument arrays, avoiding shell string concatenation.
|
||||
- Model IDs are first compared with the model list exposed by the most recent `/api/agents` response.
|
||||
- Custom model IDs are validated by `sanitizeCustomModel()`, limiting length, character set, and starting character.
|
||||
- Reasoning options must exist in the runtime definition's `reasoningOptions`.
|
||||
- Image paths must be located inside the daemon temporary upload directory.
|
||||
- Attachment paths must be located inside the project working directory.
|
||||
- Agent working directories are constrained to `.od/projects/<projectId>/`.
|
||||
- ACP runtimes have timeout protection for the initialize, session/new, session/set_model, and session/prompt stages.
|
||||
- ACP runtimes listen for `stdin` errors and proactively clean up detection processes after model detection completes.
|
||||
- When the SSE connection closes, the daemon sends `SIGTERM` to the subprocess.
|
||||
|
||||
## Current Capability Boundaries
|
||||
|
||||
The current runtime adapter is a lightweight adaptation layer that already covers discovery, startup, argument construction, model selection, and streaming forwarding.
|
||||
|
||||
Main boundaries:
|
||||
|
||||
- The adapter is still a declarative object array and has not yet been split into independent adapter classes or directories.
|
||||
- The capability model is thin and currently mainly exposes models, reasoning, and output format.
|
||||
- Claude Code, Codex, Gemini, OpenCode, Cursor Agent, Hermes, and Kimi already have structured event parsing.
|
||||
- Qwen currently still uses plain text pass-through.
|
||||
- Skill injection mainly relies on prompt composition; only Claude Code uses `--add-dir` to support reading external directories.
|
||||
- Hermes currently only integrates the core ACP text session path and has not mapped more `session/update` types into unified UI events.
|
||||
- Cancellation is triggered by HTTP connection closure and `SIGTERM`; there is no explicit runId / cancel API yet.
|
||||
- Resume, auth state, permission modes, and capability gating have not yet formed a unified interface.
|
||||
- API fallback belongs to the frontend provider path and is currently outside the daemon runtime adapter layer.
|
||||
|
||||
## Gap from the Target Architecture
|
||||
|
||||
`docs/agent-adapters.md` describes a more complete target shape: each agent adapter has interfaces such as `detect()`, `capabilities()`, `run()`, `cancel()`, and `resume()`, and outputs unified `AgentEvent`s.
|
||||
|
||||
The current implementation already has the core outline of the target architecture:
|
||||
|
||||
- `detectAgents()` corresponds to `detect()`.
|
||||
- `AGENT_DEFS` corresponds to the adapter registry.
|
||||
- `buildArgs()` corresponds to runtime-specific invocation.
|
||||
- `streamFormat` + `claude-stream.ts` + `json-event-stream.ts` + `acp.ts` correspond to stream normalization.
|
||||
- `/api/chat` corresponds to unified run orchestration.
|
||||
94
specs/current/status.md
Normal file
94
specs/current/status.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Project Status
|
||||
|
||||
## Goal
|
||||
|
||||
Show a compact status on each project card that reflects the current state of the project's most relevant run.
|
||||
|
||||
## Status source
|
||||
|
||||
Project status should be a derived display value, based on runs associated with the project.
|
||||
|
||||
The recommended logic is:
|
||||
|
||||
1. If the project has an active run, show that active run's status.
|
||||
2. If the project has no active run, show the latest run's terminal status.
|
||||
3. If the project has no runs, show `not_started`.
|
||||
|
||||
An active run takes priority over the latest terminal run because it tells the user that work is currently happening in the project.
|
||||
|
||||
## Display statuses
|
||||
|
||||
```ts
|
||||
type ProjectDisplayStatus =
|
||||
| 'not_started'
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'canceled';
|
||||
```
|
||||
|
||||
| Display status | Label | Source status | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `not_started` | Not started | No run | The project exists and has no run history. |
|
||||
| `queued` | Queued | `queued` | A run exists and is waiting to start. |
|
||||
| `running` | Running | `running`, `starting` | A run is currently executing. |
|
||||
| `succeeded` | Completed | `succeeded` | The latest relevant run completed successfully. |
|
||||
| `failed` | Failed | `failed` | The latest relevant run failed. |
|
||||
| `canceled` | Canceled | `canceled`, `cancelled` | The latest relevant run was canceled. |
|
||||
|
||||
## Derivation logic
|
||||
|
||||
```ts
|
||||
function deriveProjectDisplayStatus(projectRuns: Run[]): ProjectDisplayStatus {
|
||||
const activeRun = projectRuns
|
||||
.filter((run) => run.status === 'queued' || run.status === 'running' || run.status === 'starting')
|
||||
.sort(byMostRecent)[0];
|
||||
|
||||
if (activeRun) {
|
||||
return normalizeRunStatus(activeRun.status);
|
||||
}
|
||||
|
||||
const latestRun = projectRuns.sort(byMostRecent)[0];
|
||||
|
||||
if (latestRun) {
|
||||
return normalizeRunStatus(latestRun.status);
|
||||
}
|
||||
|
||||
return 'not_started';
|
||||
}
|
||||
|
||||
function normalizeRunStatus(status: RunStatus): ProjectDisplayStatus {
|
||||
if (status === 'starting') return 'running';
|
||||
if (status === 'cancelled') return 'canceled';
|
||||
return status;
|
||||
}
|
||||
```
|
||||
|
||||
## UI guidance
|
||||
|
||||
The project card should show the status near the existing metadata line, together with the relative timestamp when useful.
|
||||
|
||||
Examples:
|
||||
|
||||
- `Running · just now`
|
||||
- `Queued · 1 minute ago`
|
||||
- `Completed · 6 minutes ago`
|
||||
- `Failed · 36 minutes ago`
|
||||
- `Canceled · 3 hours ago`
|
||||
- `Not started`
|
||||
|
||||
Use stronger visual treatment for active and error states:
|
||||
|
||||
- `running`: primary or accent indicator.
|
||||
- `queued`: neutral pending indicator.
|
||||
- `failed`: error indicator.
|
||||
- `canceled`: muted neutral indicator.
|
||||
- `succeeded`: subtle success indicator.
|
||||
- `not_started`: muted placeholder indicator.
|
||||
|
||||
## Rationale
|
||||
|
||||
Project status represents the user's project-level mental model. Users need to know whether a project is waiting, actively running, completed, failed, canceled, or untouched.
|
||||
|
||||
Using `running` as the primary active label keeps the UI aligned with the underlying run model and covers generation, editing, repair, analysis, export, and future run types.
|
||||
Reference in New Issue
Block a user