Skip to content

Commit c0f84e7

Browse files
Merge remote-tracking branch 'upstream/dev' into dev
# Conflicts: # packages/app/src/app/components/session/composer.tsx # packages/app/src/app/components/session/message-list.tsx # packages/app/src/app/pages/config.tsx # packages/app/src/app/pages/dashboard.tsx # packages/app/src/app/pages/scheduled.tsx # packages/app/src/app/pages/session.tsx # packages/app/src/app/pages/settings.tsx
2 parents 649e6f0 + c349413 commit c0f84e7

File tree

283 files changed

+14161
-1529
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

283 files changed

+14161
-1529
lines changed

.github/workflows/ci.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,31 @@ jobs:
3434
run: pnpm install --frozen-lockfile
3535

3636
- name: Build web
37-
run: pnpm --filter @different-ai/openwork build:web
37+
run: pnpm --filter @different-ai/openwork-web build
38+
39+
build-den:
40+
name: Build Den service
41+
runs-on: ubuntu-latest
42+
43+
steps:
44+
- name: Checkout
45+
uses: actions/checkout@v4
46+
47+
- name: Setup Node
48+
uses: actions/setup-node@v4
49+
with:
50+
node-version: 20
51+
52+
- name: Setup pnpm
53+
uses: pnpm/action-setup@v4
54+
with:
55+
version: 10.27.0
56+
57+
- name: Install dependencies
58+
run: pnpm install --frozen-lockfile
59+
60+
- name: Build den service
61+
run: pnpm --filter @openwork/den build
3862

3963
build-orchestrator-binary:
4064
name: Build openwork orchestrator binary

.github/workflows/deploy-den.yml

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
name: Deploy Den
2+
3+
on:
4+
push:
5+
branches:
6+
- dev
7+
paths:
8+
- "services/den/**"
9+
- "services/den-worker-runtime/**"
10+
- ".github/workflows/deploy-den.yml"
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: deploy-den-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
deploy:
22+
runs-on: ubuntu-latest
23+
if: github.repository == 'different-ai/openwork'
24+
steps:
25+
- name: Validate required secrets
26+
env:
27+
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
28+
RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
29+
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
30+
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
31+
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
32+
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
33+
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
34+
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
35+
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
36+
run: |
37+
missing=0
38+
for key in RENDER_API_KEY RENDER_DEN_CONTROL_PLANE_SERVICE_ID RENDER_OWNER_ID DEN_DATABASE_URL DEN_BETTER_AUTH_SECRET; do
39+
if [ -z "${!key}" ]; then
40+
echo "::error::Missing required secret: $key"
41+
missing=1
42+
fi
43+
done
44+
45+
feature_enabled="${DEN_POLAR_FEATURE_GATE_ENABLED:-false}"
46+
feature_enabled="$(echo "$feature_enabled" | tr '[:upper:]' '[:lower:]')"
47+
if [ "$feature_enabled" = "true" ]; then
48+
for key in POLAR_ACCESS_TOKEN POLAR_PRODUCT_ID POLAR_BENEFIT_ID; do
49+
if [ -z "${!key}" ]; then
50+
echo "::error::Missing required paywall secret: $key"
51+
missing=1
52+
fi
53+
done
54+
fi
55+
56+
if [ "$missing" -ne 0 ]; then
57+
exit 1
58+
fi
59+
60+
- name: Sync Render env vars and deploy latest commit
61+
env:
62+
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
63+
RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
64+
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
65+
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
66+
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
67+
DEN_RENDER_WORKER_OPENWORK_VERSION: ${{ vars.DEN_RENDER_WORKER_OPENWORK_VERSION }}
68+
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
69+
DEN_POLAR_API_BASE: ${{ vars.DEN_POLAR_API_BASE }}
70+
DEN_POLAR_SUCCESS_URL: ${{ vars.DEN_POLAR_SUCCESS_URL }}
71+
DEN_POLAR_RETURN_URL: ${{ vars.DEN_POLAR_RETURN_URL }}
72+
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
73+
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
74+
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
75+
run: |
76+
python3 <<'PY'
77+
import json
78+
import os
79+
import time
80+
import urllib.error
81+
import urllib.parse
82+
import urllib.request
83+
84+
api_key = os.environ["RENDER_API_KEY"]
85+
service_id = os.environ["RENDER_DEN_CONTROL_PLANE_SERVICE_ID"]
86+
owner_id = os.environ["RENDER_OWNER_ID"]
87+
openwork_version = os.environ.get("DEN_RENDER_WORKER_OPENWORK_VERSION") or "0.11.113"
88+
paywall_enabled = (os.environ.get("DEN_POLAR_FEATURE_GATE_ENABLED") or "false").lower() == "true"
89+
polar_api_base = os.environ.get("DEN_POLAR_API_BASE") or "https://api.polar.sh"
90+
polar_success_url = os.environ.get("DEN_POLAR_SUCCESS_URL") or "https://app.openwork.software"
91+
polar_return_url = os.environ.get("DEN_POLAR_RETURN_URL") or polar_success_url
92+
polar_access_token = os.environ.get("POLAR_ACCESS_TOKEN") or ""
93+
polar_product_id = os.environ.get("POLAR_PRODUCT_ID") or ""
94+
polar_benefit_id = os.environ.get("POLAR_BENEFIT_ID") or ""
95+
96+
def validate_redirect_url(name: str, value: str):
97+
parsed = urllib.parse.urlparse(value)
98+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
99+
raise RuntimeError(f"{name} must be an absolute http(s) URL, got: {value}")
100+
101+
validate_redirect_url("DEN_POLAR_SUCCESS_URL", polar_success_url)
102+
validate_redirect_url("DEN_POLAR_RETURN_URL", polar_return_url)
103+
104+
if paywall_enabled and (not polar_access_token or not polar_product_id or not polar_benefit_id):
105+
raise RuntimeError(
106+
"DEN_POLAR_FEATURE_GATE_ENABLED=true requires POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, and POLAR_BENEFIT_ID"
107+
)
108+
109+
headers = {
110+
"Authorization": f"Bearer {api_key}",
111+
"Accept": "application/json",
112+
"Content-Type": "application/json",
113+
}
114+
115+
def request(method: str, path: str, body=None):
116+
url = f"https://api.render.com/v1{path}"
117+
data = None
118+
if body is not None:
119+
data = json.dumps(body).encode("utf-8")
120+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
121+
try:
122+
with urllib.request.urlopen(req, timeout=60) as resp:
123+
text = resp.read().decode("utf-8")
124+
return resp.status, json.loads(text) if text else None
125+
except urllib.error.HTTPError as err:
126+
text = err.read().decode("utf-8", "replace")
127+
raise RuntimeError(f"{method} {path} failed ({err.code}): {text[:600]}")
128+
129+
_, service = request("GET", f"/services/{service_id}")
130+
service_url = (service.get("serviceDetails") or {}).get("url")
131+
if not service_url:
132+
raise RuntimeError(f"Render service {service_id} has no public URL")
133+
134+
env_vars = [
135+
{"key": "DATABASE_URL", "value": os.environ["DEN_DATABASE_URL"]},
136+
{"key": "BETTER_AUTH_SECRET", "value": os.environ["DEN_BETTER_AUTH_SECRET"]},
137+
{"key": "BETTER_AUTH_URL", "value": service_url},
138+
{"key": "PROVISIONER_MODE", "value": "render"},
139+
{"key": "RENDER_API_BASE", "value": "https://api.render.com/v1"},
140+
{"key": "RENDER_API_KEY", "value": api_key},
141+
{"key": "RENDER_OWNER_ID", "value": owner_id},
142+
{"key": "RENDER_WORKER_REPO", "value": "https://github.com/different-ai/openwork"},
143+
{"key": "RENDER_WORKER_BRANCH", "value": "dev"},
144+
{"key": "RENDER_WORKER_ROOT_DIR", "value": "services/den-worker-runtime"},
145+
{"key": "RENDER_WORKER_PLAN", "value": "starter"},
146+
{"key": "RENDER_WORKER_REGION", "value": "oregon"},
147+
{"key": "RENDER_WORKER_OPENWORK_VERSION", "value": openwork_version},
148+
{"key": "RENDER_WORKER_NAME_PREFIX", "value": "den-worker-openwork"},
149+
{"key": "RENDER_PROVISION_TIMEOUT_MS", "value": "900000"},
150+
{"key": "RENDER_HEALTHCHECK_TIMEOUT_MS", "value": "180000"},
151+
{"key": "RENDER_POLL_INTERVAL_MS", "value": "5000"},
152+
{"key": "POLAR_FEATURE_GATE_ENABLED", "value": "true" if paywall_enabled else "false"},
153+
{"key": "POLAR_API_BASE", "value": polar_api_base},
154+
{"key": "POLAR_ACCESS_TOKEN", "value": polar_access_token},
155+
{"key": "POLAR_PRODUCT_ID", "value": polar_product_id},
156+
{"key": "POLAR_BENEFIT_ID", "value": polar_benefit_id},
157+
{"key": "POLAR_SUCCESS_URL", "value": polar_success_url},
158+
{"key": "POLAR_RETURN_URL", "value": polar_return_url},
159+
]
160+
161+
request("PUT", f"/services/{service_id}/env-vars", env_vars)
162+
_, deploy = request("POST", f"/services/{service_id}/deploys", {})
163+
deploy_id = deploy.get("id") or (deploy.get("deploy") or {}).get("id")
164+
if not deploy_id:
165+
raise RuntimeError(f"Unexpected deploy response: {deploy}")
166+
167+
terminal = {"live", "update_failed", "build_failed", "canceled"}
168+
started = time.time()
169+
170+
while time.time() - started < 1800:
171+
_, deploys = request("GET", f"/services/{service_id}/deploys?limit=1")
172+
latest = deploys[0]["deploy"] if deploys else None
173+
if latest and latest.get("id") == deploy_id and latest.get("status") in terminal:
174+
status = latest.get("status")
175+
if status != "live":
176+
raise RuntimeError(f"Render deploy {deploy_id} ended with {status}")
177+
print(f"Render deploy {deploy_id} is live at {service_url}")
178+
break
179+
time.sleep(10)
180+
else:
181+
raise RuntimeError(f"Timed out waiting for deploy {deploy_id}")
182+
PY

.github/workflows/release-macos-aarch64.yml

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,15 @@ jobs:
122122
RELEASE_BODY="See the assets to download this version and install."
123123
fi
124124
125-
draft="${INPUT_DRAFT:-false}"
125+
draft="${INPUT_DRAFT:-}"
126+
if [ -z "$draft" ]; then
127+
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
128+
# Keep tag-triggered releases out of /releases/latest until assets + latest.json are ready.
129+
draft="true"
130+
else
131+
draft="false"
132+
fi
133+
fi
126134
prerelease="${INPUT_PRERELEASE:-false}"
127135
notarize="${INPUT_NOTARIZE:-}"
128136
if [ -z "$notarize" ]; then
@@ -524,7 +532,7 @@ jobs:
524532
tauriScript: pnpm exec tauri -vvv
525533
args: ${{ matrix.args }}
526534
retryAttempts: 3
527-
uploadUpdaterJson: true
535+
uploadUpdaterJson: false
528536
updaterJsonPreferNsis: true
529537
releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]
530538

@@ -553,7 +561,7 @@ jobs:
553561
tauriScript: pnpm exec tauri -vvv
554562
args: ${{ matrix.args }}
555563
retryAttempts: 3
556-
uploadUpdaterJson: true
564+
uploadUpdaterJson: false
557565
updaterJsonPreferNsis: true
558566
releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]
559567

@@ -600,6 +608,39 @@ jobs:
600608
601609
echo "Found bundled versions.json at $manifest_path"
602610
611+
publish-updater-json:
612+
name: Publish consolidated latest.json
613+
needs: [resolve-release, verify-release, publish-tauri]
614+
if: needs.resolve-release.outputs.build_tauri == 'true'
615+
runs-on: ubuntu-latest
616+
env:
617+
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
618+
steps:
619+
- name: Checkout
620+
uses: actions/checkout@v4
621+
with:
622+
ref: ${{ env.RELEASE_TAG }}
623+
fetch-depth: 0
624+
625+
- name: Generate latest.json from release assets
626+
env:
627+
GH_TOKEN: ${{ github.token }}
628+
run: |
629+
set -euo pipefail
630+
node scripts/release/generate-latest-json.mjs \
631+
--tag "$RELEASE_TAG" \
632+
--repo "$GITHUB_REPOSITORY" \
633+
--output "$RUNNER_TEMP/latest.json"
634+
635+
- name: Upload latest.json
636+
env:
637+
GH_TOKEN: ${{ github.token }}
638+
run: |
639+
set -euo pipefail
640+
gh release upload "$RELEASE_TAG" "$RUNNER_TEMP/latest.json#latest.json" \
641+
--repo "$GITHUB_REPOSITORY" \
642+
--clobber
643+
603644
release-orchestrator-sidecars:
604645
name: Build + Upload openwork-orchestrator Sidecars
605646
needs: [resolve-release, verify-release]
@@ -848,7 +889,12 @@ jobs:
848889
849890
aur-publish:
850891
name: Publish AUR
851-
needs: [resolve-release, publish-tauri]
892+
needs: [resolve-release, publish-tauri, publish-release]
893+
if: |
894+
always() &&
895+
needs.resolve-release.result == 'success' &&
896+
(needs.publish-tauri.result == 'success' || needs.publish-tauri.result == 'skipped') &&
897+
(needs.publish-release.result == 'success' || needs.publish-release.result == 'skipped')
852898
runs-on: ubuntu-latest
853899
permissions:
854900
contents: write
@@ -893,3 +939,38 @@ jobs:
893939
exit 0
894940
fi
895941
scripts/aur/publish-aur.sh "$RELEASE_TAG"
942+
943+
publish-release:
944+
name: Publish GitHub Release
945+
needs:
946+
- resolve-release
947+
- verify-release
948+
- publish-tauri
949+
- publish-updater-json
950+
- release-orchestrator-sidecars
951+
- publish-npm
952+
if: |
953+
always() &&
954+
needs.resolve-release.outputs.draft == 'true' &&
955+
needs.resolve-release.result == 'success' &&
956+
needs.verify-release.result == 'success' &&
957+
(needs.publish-tauri.result == 'success' || needs.publish-tauri.result == 'skipped') &&
958+
(needs.publish-updater-json.result == 'success' || needs.publish-updater-json.result == 'skipped') &&
959+
(needs.release-orchestrator-sidecars.result == 'success' || needs.release-orchestrator-sidecars.result == 'skipped') &&
960+
(needs.publish-npm.result == 'success' || needs.publish-npm.result == 'skipped')
961+
runs-on: ubuntu-latest
962+
env:
963+
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
964+
RELEASE_PRERELEASE: ${{ needs.resolve-release.outputs.prerelease }}
965+
steps:
966+
- name: Publish release after assets are ready
967+
env:
968+
GH_TOKEN: ${{ github.token }}
969+
run: |
970+
set -euo pipefail
971+
972+
if [ "${RELEASE_PRERELEASE}" = "true" ]; then
973+
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease
974+
else
975+
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest
976+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ vendor/opencode/
3939

4040
# OpenWork workspace-local artifacts
4141
.opencode/openwork/
42+
.vercel

.opencode/skills/openwork-docker-chrome-mcp/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ Evidence:
4747
- Take a Chrome MCP screenshot after the response appears.
4848
- If something fails, capture console logs and (optionally) Docker logs.
4949

50+
### Verification checklist (copy into PR)
51+
52+
- [ ] Started stack with `packaging/docker/dev-up.sh` from repo root.
53+
- [ ] Used the printed Web UI URL (not a guessed port).
54+
- [ ] Completed one full user flow in the UI (input -> action -> visible result).
55+
- [ ] Captured at least one screenshot for the success state.
56+
- [ ] Captured failure evidence when relevant (console and/or Docker logs).
57+
- [ ] Stopped stack with the exact printed `docker compose -p ... down` command.
58+
59+
Suggested screenshot set for user-facing changes:
60+
- Before action state.
61+
- During action/progress state.
62+
- Success state.
63+
- Failure or recovery state (if applicable).
64+
5065
### 3) Stop the stack
5166

5267
Use the exact `docker compose -p ... down` command printed by `dev-up.sh`.

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ Read INFRASTRUCTURE.md
3737
* **Slick and fluid**: 60fps animations, micro-interactions, premium feel.
3838
* **Mobile-native**: touch targets, gestures, and layouts optimized for small screens.
3939

40+
## Task Intake (Required)
41+
42+
Before making changes, explicitly confirm the target repository in your first task update.
43+
44+
Required format:
45+
46+
1. `Target repo: <path>` (for example: `_repos/openwork`)
47+
2. `Out of scope repos: <list>` (for example: `_repos/opencode`)
48+
3. `Planned output: <what will be changed/tested>`
49+
50+
If the user request references multiple repos and the intended edit location is ambiguous, stop after discovery and ask for a single repo target before editing files.
51+
4052
## New Feature Workflow (Required)
4153

4254
When the user asks to create a new feature, follow this exact procedure:

STATS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@
2727
| 2026-02-14 | 72,204 (+2,676) | 72,204 (+2,676) |
2828
| 2026-02-15 | 74,561 (+2,357) | 74,561 (+2,357) |
2929
| 2026-02-16 | 77,144 (+2,583) | 77,144 (+2,583) |
30+
| 2026-02-17 | 79,817 (+2,673) | 79,817 (+2,673) |
31+
| 2026-02-18 | 83,020 (+3,203) | 83,020 (+3,203) |
32+
| 2026-02-19 | 86,687 (+3,667) | 86,687 (+3,667) |
33+
| 2026-02-20 | 90,491 (+3,804) | 90,491 (+3,804) |
34+
| 2026-02-21 | 94,409 (+3,918) | 94,409 (+3,918) |

0 commit comments

Comments
 (0)