From 49cba9e6c290345b74012efccf2c5d5030e7d7f2 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Mon, 9 Mar 2026 16:34:27 -0700 Subject: [PATCH] feat(factory): finish workbench milestone pass --- .dockerignore | 2 +- .gitignore | 2 +- .../sqlite/handoff/1513aa2dffb2aeb1.sqlite | Bin 0 -> 4096 bytes .../handoff/1513aa2dffb2aeb1.sqlite-shm | Bin 0 -> 32768 bytes .../handoff/1513aa2dffb2aeb1.sqlite-wal | Bin 0 -> 90672 bytes .../sqlite/history/2ee2a83a8f00bf51.sqlite | Bin 0 -> 20480 bytes .../sqlite/history/83fb73090cf32cca.sqlite | Bin 0 -> 20480 bytes .../sqlite/project/27bd62398cfef512.sqlite | Bin 0 -> 40960 bytes .../sqlite/project/6d0adb3e91e6d8af.sqlite | Bin 0 -> 40960 bytes .../sqlite/project/86bea07dcf41d624.sqlite | Bin 0 -> 65536 bytes .../sandbox-instance/040d32046643c32d.sqlite | Bin 0 -> 4096 bytes .../040d32046643c32d.sqlite-shm | Bin 0 -> 32768 bytes .../040d32046643c32d.sqlite-wal | Bin 0 -> 57712 bytes .../sandbox-instance/33da3db915764968.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/3715079ac70cc298.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/44352c1fd428a7da.sqlite | Bin 0 -> 4096 bytes .../44352c1fd428a7da.sqlite-shm | Bin 0 -> 32768 bytes .../44352c1fd428a7da.sqlite-wal | Bin 0 -> 57712 bytes .../sandbox-instance/5c43c54a5a4efcbd.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/70b6da466222ac43.sqlite | Bin 0 -> 4096 bytes .../70b6da466222ac43.sqlite-shm | Bin 0 -> 32768 bytes .../70b6da466222ac43.sqlite-wal | Bin 0 -> 57712 bytes .../sandbox-instance/9ed3b93c064512c4.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/afbe363197a91921.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/d06bdd0ae7a34f37.sqlite | Bin 0 -> 45056 bytes .../sandbox-instance/e3f12a890eaebaac.sqlite | Bin 0 -> 4096 bytes .../e3f12a890eaebaac.sqlite-shm | Bin 0 -> 32768 bytes .../e3f12a890eaebaac.sqlite-wal | Bin 0 -> 57712 bytes .../sqlite/workspace/57c45274e0331bab.sqlite | Bin 0 -> 36864 bytes .../sqlite/workspace/d506cab654089c0a.sqlite | Bin 0 -> 36864 bytes factory/CLAUDE.md | 16 ++- factory/CONTRIBUTING.md | 6 +- factory/Dockerfile | 10 +- factory/README.md | 4 +- factory/compose.dev.yaml | 72 +++++----- factory/compose.preview.yaml | 16 +-- factory/docker/backend.dev.Dockerfile | 2 +- factory/docker/backend.preview.Dockerfile | 6 +- factory/docker/frontend.dev.Dockerfile | 2 +- factory/docker/frontend.preview.Dockerfile | 8 +- factory/packages/backend/package.json | 4 +- .../packages/backend/src/actors/context.ts | 2 +- factory/packages/backend/src/actors/events.ts | 2 +- .../packages/backend/src/actors/handles.ts | 2 +- .../src/actors/handoff-status-sync/index.ts | 2 +- .../backend/src/actors/handoff/index.ts | 2 +- .../src/actors/handoff/workflow/common.ts | 2 +- .../src/actors/handoff/workflow/index.ts | 8 +- .../backend/src/actors/history/index.ts | 2 +- .../packages/backend/src/actors/logging.ts | 2 +- .../backend/src/actors/project/actions.ts | 6 +- .../src/actors/sandbox-instance/index.ts | 2 +- .../backend/src/actors/workspace/actions.ts | 2 +- .../packages/backend/src/config/backend.ts | 4 +- .../packages/backend/src/config/workspace.ts | 2 +- .../packages/backend/src/db/actor-sqlite.ts | 4 +- .../backend/src/integrations/git/index.ts | 2 +- .../src/integrations/sandbox-agent/client.ts | 4 +- .../backend/src/providers/daytona/index.ts | 24 ++-- .../packages/backend/src/providers/index.ts | 4 +- .../backend/src/providers/local/index.ts | 2 +- .../src/providers/provider-api/index.ts | 2 +- ...{openhandoff-paths.ts => factory-paths.ts} | 8 +- .../backend/test/daytona-provider.test.ts | 8 +- .../backend/test/git-validate-remote.test.ts | 2 +- .../backend/test/helpers/test-context.ts | 2 +- .../packages/backend/test/providers.test.ts | 4 +- .../backend/test/repo-normalize.test.ts | 26 ++-- .../backend/test/workspace-isolation.test.ts | 2 +- .../packages/backend/tmp-decode-actors.mjs | 2 +- factory/packages/backend/tmp-dump-wfkeys.mjs | 2 +- factory/packages/backend/tmp-inspect-deep.mjs | 2 +- .../packages/backend/tmp-inspect-stuck.mjs | 2 +- .../packages/backend/tmp-inspect-workflow.mjs | 2 +- factory/packages/cli/package.json | 6 +- factory/packages/cli/src/backend/manager.ts | 10 +- factory/packages/cli/src/index.ts | 4 +- factory/packages/cli/src/theme.ts | 4 +- factory/packages/cli/src/tui.ts | 6 +- factory/packages/cli/src/workspace/config.ts | 4 +- .../packages/cli/test/backend-manager.test.ts | 4 +- factory/packages/cli/test/theme.test.ts | 8 +- factory/packages/cli/test/tui-format.test.ts | 4 +- .../cli/test/workspace-config.test.ts | 4 +- factory/packages/client/package.json | 37 ++++- factory/packages/client/src/backend-client.ts | 2 +- factory/packages/client/src/backend.ts | 1 + .../client/src/mock/workbench-client.ts | 45 ++++-- .../client/src/remote/workbench-client.ts | 7 +- factory/packages/client/src/view-model.ts | 2 +- .../packages/client/src/workbench-client.ts | 7 +- .../packages/client/src/workbench-model.ts | 7 +- factory/packages/client/src/workbench.ts | 1 + .../test/e2e/full-integration-e2e.test.ts | 2 +- .../client/test/e2e/github-pr-e2e.test.ts | 2 +- .../client/test/e2e/workbench-e2e.test.ts | 71 +++++++++- .../test/e2e/workbench-load-e2e.test.ts | 8 +- .../packages/client/test/view-model.test.ts | 2 +- .../client/test/workbench-client.test.ts | 128 ++++++++++++++++++ factory/packages/frontend-errors/package.json | 2 +- .../packages/frontend-errors/src/client.ts | 10 +- .../packages/frontend-errors/src/router.ts | 4 +- .../packages/frontend-errors/src/script.ts | 10 +- factory/packages/frontend-errors/src/vite.ts | 6 +- .../frontend-errors/test/router.test.ts | 4 +- factory/packages/frontend/index.html | 2 +- factory/packages/frontend/package.json | 8 +- factory/packages/frontend/src/app/router.tsx | 36 +++-- .../frontend/src/components/mock-layout.tsx | 100 ++++++++------ .../components/mock-layout/right-sidebar.tsx | 60 +++++++- .../src/components/mock-layout/sidebar.tsx | 30 +++- .../components/mock-layout/view-model.test.ts | 2 +- .../src/components/mock-layout/view-model.ts | 2 +- .../src/components/workspace-dashboard.tsx | 5 +- .../src/factory-client-view-model.d.ts | 7 + .../src/features/handoffs/model.test.ts | 2 +- .../frontend/src/features/handoffs/model.ts | 2 +- .../src/features/sessions/model.test.ts | 2 +- .../frontend/src/features/sessions/model.ts | 4 +- factory/packages/frontend/src/lib/backend.ts | 2 +- factory/packages/frontend/src/lib/env.ts | 6 +- .../frontend/src/lib/workbench-routing.ts | 8 ++ .../src/lib/workbench-runtime.mock.ts | 11 ++ .../src/lib/workbench-runtime.remote.ts | 13 ++ .../frontend/src/lib/workbench.test.ts | 38 ++++++ .../packages/frontend/src/lib/workbench.ts | 25 ++-- factory/packages/frontend/tsconfig.json | 6 +- factory/packages/frontend/vite.config.ts | 20 ++- factory/packages/shared/package.json | 2 +- factory/packages/shared/src/config.ts | 4 +- .../packages/shared/test/workspace.test.ts | 2 +- factory/research/friction/general.mdx | 6 +- factory/research/friction/rivet.mdx | 8 +- .../specs/rivetkit-opentui-migration-plan.md | 2 +- factory/tsconfig.base.json | 10 +- justfile | 16 +-- pnpm-lock.yaml | 10 +- 137 files changed, 819 insertions(+), 338 deletions(-) create mode 100644 .openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite create mode 100644 .openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm create mode 100644 .openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal create mode 100644 .openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite create mode 100644 .openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite create mode 100644 .openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite create mode 100644 .openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite create mode 100644 .openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/33da3db915764968.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/3715079ac70cc298.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-wal create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/afbe363197a91921.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/d06bdd0ae7a34f37.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm create mode 100644 .openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-wal create mode 100644 .openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite create mode 100644 .openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite rename factory/packages/backend/src/services/{openhandoff-paths.ts => factory-paths.ts} (65%) create mode 100644 factory/packages/client/src/backend.ts create mode 100644 factory/packages/client/src/workbench.ts create mode 100644 factory/packages/client/test/workbench-client.test.ts create mode 100644 factory/packages/frontend/src/factory-client-view-model.d.ts create mode 100644 factory/packages/frontend/src/lib/workbench-routing.ts create mode 100644 factory/packages/frontend/src/lib/workbench-runtime.mock.ts create mode 100644 factory/packages/frontend/src/lib/workbench-runtime.remote.ts create mode 100644 factory/packages/frontend/src/lib/workbench.test.ts diff --git a/.dockerignore b/.dockerignore index cb03545..5625bc9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,7 +17,7 @@ coverage/ # Environment .env .env.* -.openhandoff/ +.sandbox-agent-factory/ # IDE .idea/ diff --git a/.gitignore b/.gitignore index da6874a..c8153ca 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,7 @@ Cargo.lock # Example temp files .tmp-upload/ *.db -.openhandoff/ +.sandbox-agent-factory/ # CLI binaries (downloaded during npm publish) sdks/cli/platforms/*/bin/ diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..159e90adb13561c0365c22e76084c658383a62f1 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|1_}TpgI@11UXTF-AYv4chQMeD mjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb22M2mk=1QwNv; literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..d319f4dc0d2cd81a6bc331ed97942589472039ac GIT binary patch literal 32768 zcmb1mq{{#T+zbp1j0_9{VhjunoD2*MZYO$wdv7(n@s?NA;m6kdS43j!r@F~JpGc~D zkl7#%GWS0c0Er=EMg|53CI$uuW{5lBY!(Iv237_J1~vu;26hGp1`a3&iGkSIXpmVT zeIS1z+cC->4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R z7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7 zfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z z5Eu=C(GVC7fzc2cY$3qNz``Ih*c>(Lp3xAXO9(K5PgbFeRil=ShQJ^T0cP--LW9gv zqs|!(0lI_$JA)`)>>0IWGz11;2yig)GVn79G6)Yor;WO3Gz4fI0;A_5(l~%ejUU7z zz%_{7HtMv|5TI=ca5L~Q@G%H52+`J>QPW35U=W4?=oIQf=&VsEjfTKz2#kinXb6mk zz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By f2#kinXb6mkz-S1JhQMeDjE2By2#kgR86f}wapWnQ literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal new file mode 100644 index 0000000000000000000000000000000000000000..37771404d7e8ce71ceecac72f1aa597dfe00ebb6 GIT binary patch literal 90672 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 literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite b/.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..4dd06a1e4549cee0eea12e46a2ddb78314a20468 GIT binary patch literal 20480 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 IC6;|X00RX__W%F@ literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..159e90adb13561c0365c22e76084c658383a62f1 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|1_}TpgI@11UXTF-AYv4chQMeD mjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb22M2mk=1QwNv; literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..8f433e6b8b81e0558f43d871b4e70959c102138c GIT binary patch literal 32768 zcmb1mq{{#TObiSRj0_9{d<+Z>+zbp19>>(!W_!6mpToG?Fp<}oui-)XYQgS3zerUN zG8=?J=KeLI8BOFEz{=HDfdchF%Ep3_T~0x_C4Ms1yRcR5EAOn9&dzdLaP1fneyldep_E zAut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? b8UmvsFd71*Aut*OqaiRF0;3^7Ob7q~$)_W% literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal new file mode 100644 index 0000000000000000000000000000000000000000..eed2dd43475a52689e5d3d5f7d8b3d14e8fddc94 GIT binary patch literal 57712 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 literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..b0453eb39d73646129aaf97dafc6333596a2d8e8 GIT binary patch literal 32768 zcmb1mq{{#TObiSRj0_9{d<+Z>+zbp1>r`96Y-V~NV85kJY7#JAX85kHi7#J8h85kJ2pzcIA zW0XA_0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF z0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71* zAut*OqaiTVLV%G0biTw;bM>fuM?-)bA;1JaS%n%VjhZnU0z)kXn89ZX4K-Jfx_2}L zs1X97v$UvT&ZrrqAu#kp0CbAn&~x>ui$_C%8X*8WKam>djG8eT0z)qZKqrR|Jy(yq zcr*m45dxsIeW_v2s2QUnF!VxzXXrV3)WxGAK&24irII+-#GcT5ot4)n<^ zNmWS8FUn0UQ7~o@U|?cma8OWSU|?WkU|?Vd>49R9eikSjM1lAq8Xsn2(Chuh%m0Ic ziJzH)pP7F>KE(@?&&^CPN-W9D&nw0z#^fC2>KNjx5aQ_M+Akf8(+YZQ-$z-S1JhQMeDjE2By2#kinXb6mk zz-S1JhQMeDjE2By2n^2<0F4na!^Q=8qOK;FykobcJ^HP%XE8>e&i;FY!^Wsy>Qu9iR;oQuWc*LsD_{@|F?CQCp>rOrW zTwEg*60j@6ZD@jmzaK7L2?`nsFvn%4B@Z$=*!72o^Q$6SWl#9z}|}oE5_{` zE>eAiW-3YELDHL`*~rhqF7B<%*k}xPB3d{WvP3~*Wlnx#N_y6n|Qn>u65Da!z|tqyEsRa3Jbfqt}bJ9HMV#tM$=WyiZ|>*vCPFuIMfrcUl3H9+lwS#6a-IN-IgkV31wvkC2~yy5Y1*^0 zi3jTv8Ro?ppB zVo4&BZ%~qOaY z;NQ%jHgFa3sLi7xFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRHLcp7em7$TB z)xgZiBsJC8Jju{F+0fWD+1xZW$vo90$_Dhj9|kOO%e?)jZ72GEX>SPjZzW~QqxRK(u~cG%*{;9 zk_}Q)(#(v^Q<6>1l1vlRER9mkOpTKbER!wF(-O_i4O5I6=L84VnZV8vgsm5Ne^2Yq zcg{W?&^`ht{_hO@fB3%-hZq?3!)OSMhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By z2#kgRJOo&n85o(EnHe}?djMeL0y7(%+sdZB+YMST0KLKhu4$Ap8UmvsFd71*Aut*O zqaiRF0;3@?8UmvsFd71*Aut*OqaiTtLVy+IBiOhAqh8&IGrR7zjgAX|LS+<>hQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinunz$?kPl(w0%g0diA-nd;sNa=7+o&_ z3Yk$n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O!#V^&_X)ty9X!DrdXM#( zfAr{jfngozqn;ZLfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!3hf2ylQHu=N6Z zw!BWaPxYFPt``7>$|xQUfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!8489|EAW z2jS}l?z>mCP0&1GJ~}Qi?BjjZgQFoZ8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71* zApi;i@DKoOy+H5!g|BR_FE@e(1whiHcr*k?Ltr!nMnhmU1V%$(Gz3ONU^E0qLtr!n zMnhmU1V%$(7>58i0|Ns$Y+T?=tLnO#_e`5d#|4IQjE{P4Gz3ONU^E0qLtr!nMnhmU X1V%$(Gz3ONU^E0qLtr!npdkPNj2C&O literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8614625596be9c7db2088ae3e51f73d1355309a7 GIT binary patch literal 45056 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|?ckVBlsz0Colj1{MUDff0#~i^;^G z*ZYf?{|5sXrzitIGyi%%6P`1?pSfLl>bW*?KH?O`rEyezGz3ONU^E0qLtr!nMnhmU z1n3$9jb2>r;<=WLjn$cXDXA63iFqkW`4#cSsl~;a`FZiFWvO{3#c*zBN_=urYGO%h zN_=8Td}ay`_1w-uu8twD3Z8y0t`Q0eq#Bx_;O~b^SAv2@0?culDG53W2=^ptHu`e1 zi|1Q1Hr5mA8L;=_!HRMFhKp3+pqWaNcaZcZXg2b5u#0=^GBz56oro5W#rWf*m=mwN z(RE{WWicqYkwdu2f{k6=(vq=-yCg9wCl$>_(By($lpX4v5Jx8;S3J?n#hH+qlAutM zT2Z195aj9W7!;}C?HZ}z=O3cr7wY4q!^Mf50w4;JC6R+rAv3QeH9fTmMK_X%u*>J? z<)tQgHOdwnd#Y;+RaY<2rCAjcOfW;ih0q_DLFS7(G@VPYYS=q#cb%_k~ zVvNu(W^43e28VN#H%d5X<`tJD<|V_^Ka9_UJ)B|E2?`p}aE2vITnRikwInemu_O`6 zHz-NCxFoTpv=~c3mlmWzi)VNU!8LMeHfl1mi<_D9# zO7X?m#F$XLrG!n163AHo{m1qc~^&vlarG(Be6IGBe!yLaw4K#A+Izy zwJ0+gq+HXTiA_A(6j$6~b3?o#Hc@5<1_lmLoyrU9=<}yB@Nec%1Mx@kXb6mkz-S1J zhQMeDjE2By2#kinXb6mkz-S1JhQMeDjD`TYA>hr#%FxKmYG7t$lA3C4o@8j8Y-ntn zY;KyGWS(l0WNDsiU~Fh$_2XqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71bG6Yzd85o(EnHe}i^ZyJ2gVK$ojv5Vt(GVC7 zfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@Rpi&5o_W!9AkfX+ohQMeDjE2By2#kin zXb6mkz-S1JhQMeDjE2By2#kinAPxc0`v1}S|3Mr7U}9o$P*7lCU|@t|1_}TpgI@11UXTF-AYv4chQMeD mjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb22M2mk=1QwNv; literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..5479a93a98deb65e2fbf7fb3b87f6ba86391dede GIT binary patch literal 32768 zcmb1mq{{#TObiSRj0_9{d<+Z>+zbp15iPvQ$=L_*ukl^Ob@N@Dcg4vCW|1blBuG^c zG8=?J=KeLI8BOFEz{=HDfdchF%Ep3_T~0x_C4Ms1yRcR5EAOn9&dzdLaP1fneyldep_E zAut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? b8UmvsFd71*Aut*OqaiRF0;3^7Ob7q~Tb(9I literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal new file mode 100644 index 0000000000000000000000000000000000000000..3beca89e66691dadcd9f6e48477996610934c680 GIT binary patch literal 57712 zcmXr7XKP~6eI&uaAiw|uYkb#m-F(;PeSdPm{>Mz>whRmmjL715W4`dW99|z99O#o- zlB$rFUzD3zqF~G*z`(@B;Gm$uz`(%7z`(!^(gVdH{VY&6hyw9JG(OD4px670m;VO? z6F)NpKQsS&e2PcqMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONfQBK^sL9AK zZfeTdSXz>pl#?1CpHh@rRh5$(pPQLplvt9PpI3}cjLA93)iK0XA;i(i$5lZIn-V1j z4K7YjPR`5}g<#hpPe&hxfFMs_$Dl|BZ`Vj2uvA83afU)kYDI~HpMQvgU#O1{L_E1D zHL)Z$B|fo4A+IzywJ0+gq+HXTiA_A(l#wAbFD132xG*QPBsD&z0<)8lvj_x>Rp7s#)p;{u>i7{#L@Fd71*Aut*O zqaiRF0;3@?8UmvsFd71*Aut*OqaiSyLjb()A2u#Sg zb8(GONWiWLx1k9N{(iW0B`9blz#NyElAx1-a8H6}qc11Bc)lfLV?B|c0edeVtQfa% zxJdO4nyDmt2T5;&W+OibySTS5W1}(HiD=J?<)tQrR#nHtU4m>uacW*lY7x|l$PxvKl{xu|De+mw`FUv4 zT$p7)@6(E8egN#WEKs;ZRRZPt7Yq z^Op`6X98FpDLZ3Pl9O0m5}!;UTnWWXN@{UQQGO+O$$0`S=0FaB7YKQoB}jqKrD@N~ zCLXLyWSAFYgmy7oqYpDUoSVE+!Z|aqxFj(zITaSpFg^?RaE3`EQa3DF;!5DTsU?Xi zi6x0hzClUC#U+U)rNvkRy0jn#x+otWLU4^-nxp#&7#QID2sUl)*cNia|0QT20TX{3 z1OI0Jw1KOLM{OPrfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85p5CYyztPG93 ztOjOACaI~$=1GRe$%e+J$>yf1N#?00NtWiR2F8Y#X6D9*mPr;yrb(uT#%302DaHmC z$>xSBrpc)$CMGEsmW*@f%`#{3W&|6SXp(4XX=IvcW?^QYYLt>_keX&NB6z!BN|N*c6Y0D6T1T+=9HGz3ONU^E0qLtr!n zMnhmU1V%$(Gz3ONU^E0qLtr!nMnhoOg#ataN3d}L^E)Z41OyL>jgAX|LS+<>hQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinunz$?kPl(w0vc8N%k=i1Is@8AFuGm< z6f&cDGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nhII&l?h}BYJ9zI9e+K&- zaqrRf0>e7aM?E(h0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71|5a0kYVCx0q zEYwm`m77CH*9(9`WfYHwz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2Cl4*}5G zgYfkN$8Wz&IP~ya`{=m9u#fjq4~~YwXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1J zh5#r8z(WAA^#ZNAg)*~uJZA(83V@_X@n{H)hQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2By2#kinFb)B31_lOh*tkG4Z%f4egW1WW;{wAt#z(z28UmvsFd71*Aut*OqaiRF X0;3@?8UmvsFd71*Aut*O&=3Fsx?+Xn literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8614625596be9c7db2088ae3e51f73d1355309a7 GIT binary patch literal 45056 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 literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..5f6c5df19a7433b9b6d130772e8aa9bd33073e9d GIT binary patch literal 32768 zcmb1mq{{#TObiSRj0_9{d<+Z>+zbp1=OeR@Buz=a@p`gTd11*4*(HxB#ZA6+CWln@ zAhSUjWbS_?01`vSj0_A6AU7~W+yQ5^FfcH%GB7Z(F)%Q&GcYi4FfcH1GB7Z3LEVXL z#wdF<1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ON zU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU z1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0q zLtr!nMnhnzg#aT1=zNKx=IT-Rj)nj=LVyW;vI;d!8Z~1y1cq7&FoVw&8fvZ{b?;~h zP$L9DXK7KxoKZ7ILtyBI0O%CCq37yR7mtPjH9`P%ej+u@88u@x1cqJ+fKCn_dafRI z@n{H8BLqNa`%=T4Q8PwEVCaPa&(L%7sEbEKfJ!02OC@tgjTsGrp%((68wiG;t4Cct z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O bqaiRF0;3@?8UmvsFd71*Aut*O#Do9S#cFfcJOI4CGEFfcGNFfcHK^guC4KMRx%qCk8QjSn+1==J{M<^RFJ z#LvvY&&8d)|Kw|{VuyFz3N2(U<7{0vZ0{L}xTmTdbqj)p~MnhmU1V%$( zGz3ONU^E0qLtr!nMnhmU1V%$(Gz5lo2!Pl9!^Q>d*f{T0-Lg_8u#bQnDL~j67#PUj zN5I7?3f@P+XTo!a_cON(Pd(Qr&PSZ0Nal|+M?+vV1V%$(Gz3ONU^E0qLtx-SpwWwq zT|C#4v9TJwKC?J6FC{6zBEC4axHvOEFFv&_HLs)?&dp4TN38mc&rGSnuAUpZ?$p!I z#Wg}90lOmHh9)TZ`{B}+prDZeb6jRhf=&X$JqenPzMSmh`Id~0^+b9G?7eufV%)yr zBGor&rjq0xB)timjr<(!;@-N9jmBUnqJ?8I{MNlUqOB5tl=Hw@)#Ag-f=b=e+ zX?nA>iN{;wS{IEy%;F8Pi*q!ou&|5k>M}N0V~dAkG+o84c*7nP%UqmB@{0ysl_El`IX=$=LxWw133U*AmnA1AO${`radd0 zc(5*!VP1?8+Qn>*KFr{7Zt_M6=ghp~lEl2^R9HB}_$=7N877TL-LPbdD}m>xmL#Sm zmLwwi1|mn4>y7GnwM(t;G|qI`G=!8LMej_xC1V1VxLnAM% zftis>YO1k$lA&?3p|NSQxoK*Wd8$d0rFp7>v7x1zxv`;Tl7*3JlBuDwnMGQPv4KUh zxnYWFa;k}mNs5IfFP9nHia=NC7PQXrWiBM2@b3?ft?`;TQ3m$u}${i zR{LtuJ_08G?+pBZ_`eT_7#Q`#Xb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjD`R_1X!3E7@3%v88~5k0AS++UF#Qfn+c2Rfz}H^uP}gX8fA=zz-S1JhQMeDjE2By z2#kinXb6mkz-S1JhQMeDjE2By2n@RrU1+oc!6*aRE@MjN;J{7!85Z z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVE+A;1RmA#7ZLOW5GzvB0z9pnU|R>jgj| zGm1w;U^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhm&hXCk40ro_0v+-L}lhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2*5&s1H^!>7r1vQ zwmjpe%#6|X0-#VC#iJoG8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Au#Mi0Ce^s ze7!(gfzc2c4S~@R7!85Z5Eu=C W(GVC7fzc2c4S~@R7!3hv2mk<@#&{n9 literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite b/.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..0a739086796c34d8f3d6fcda56551fdcdeab295d GIT binary patch literal 36864 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|?ckVBlmx02T%Y1`vjcFv1vkXeI`| z-e0`@KNvW8!WsCP`PcI?@g3k*=Dp4n&hre9zEP>s5Eu=C(GVC7fzc2c4S~@R7!85Z z5Eu=Cp%Vg)JnZb^nwpG_!6k`FIjI?mc`5m6Y4JJv`Profs5}nmAXmo_SA`HqCm&aZ z1XRfc1r07vxL9UNfP^5ylYovmoe~5x#sE>~h7iU6IYC(QHTop`) zOVgTzP269Tks&iLCAFfsFekGlH9oPlBp=L=N3}BE5S7o_XvoSgE-ud4SMT9gCz7R8go4BnwE)Rp8R~&B$rq~;;nc2nl^%bTTiu3cZ1O$GMIc`Syh#j8lRh)UX)mpnV(mTO^gX87?iLnQ39o2PEO9u6op{dAWugh z={1gqMMVftg>Dfqyf98owk+WE78vz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2An3IT6sR)$7iR)b^{1H%;alvHy=qZCUsa|`3N#59v6a|6?~R5JrpvqYn`v}B9q zWFu4aRD%>F%fwVO<5Y_z3qxar)Fh)6Q^q;Fy?q$GnZSk_CYmK%n57vTnj0sYn50>l zTO?W-n;In=8W~wwBqkdtnXL%&IwsJ zX@fT-*sxTC6jM|4$)MNugBVz;8G-IRWG_#Zx6H8+=Q^T|r z!(>B)G!qj8kgQQsnz6Ad za(=FUQD#|ciEc`2nSOC%UP@Aag>GVcYF>$6dS(e|=HL1XgoMohzhU5iGq^lD>YC9I z7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC70rEmXm|2$-edGW#|9_l;|2TPO zj_Mi>fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5TIKKm@_hm>ZT;6S|*t% znd+LS7^mu*7#bz&TAG*}>!u``rI;k87#UkyrlR-%A@lz?82E3{&8?#rjfTKz2#kin qXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S0iDFoD+Lpg~V4*&pfpWSx= literal 0 HcmV?d00001 diff --git a/.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite b/.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..73824cbfc2e5155affcb7c7dddaff8ad4c555add GIT binary patch literal 36864 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|?ckVBlmx02T%Y1`vjcFv1vkXeI`| z-e0`@KNvW8!WsCP`PcI?@g3k*=Dp4n&hre9zEP>s5Eu=C(GVC7fzc2c4S~@R7!85Z z5Eu=Cp%Vg)JnZb^nwpG_!6k`FIjI?mc`5m6Y4JJv`Profs5}nmAXmo_SA`HqCm&aZ z1XRfc1r07vxL9UNfP^5ylYovmoe~5x#sE>~h7iU6IYC(QHTop`) zOVgTzP269Tks&iLCAFfsFekGlH9oPlBp=L=N3}BE5S7o_XvoSgE-ud4SMT9gCz7R8go4BnwE)Rp8R~&B$rq~;;nc2nl^%bTTiu3cZ1O$GMIc`Syh#j8lRh)UX)mpnV(mTO^gX87?iLnQ39o2PEO9u6op{dAWugh z={1gqMMVftg>Dfqyf98owk+WE78vz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2An3IT6sR)$7iR)b^{1H%;alvHy=qZCUsa|`3N#59v6a|6?~R5JrpvqYn`v}B9q zWFu4aRD%>F%fwVO<5Y_z3qxar)Fh)6Q^q;Fy?q$GnZSk_CYmK%n57vTnj0sYn50>l zTO?W-n;In=8W~wwBqkdtnXL%&IwsJ zX@fT-*sxTC6jM|4$)MNugBVz;8G-IRWG_#Zx6H8+=Q^T|r z!(>B)G!qj8kgQQsnz6Ad8CACZ+dYGkdNosLPW?s5pdS(gZoGTSMb0H*X{-2Tm4Fmt1 zp%MtA?i>w)(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@Rz!w6-%(|TDV+Wx5 z|IzjT_(E+|ZZrf&Ltr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1crVHfad>4`~O2f r9!GsJ8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O_(A{x1QM*# literal 0 HcmV?d00001 diff --git a/factory/CLAUDE.md b/factory/CLAUDE.md index 689b33c..c34c64c 100644 --- a/factory/CLAUDE.md +++ b/factory/CLAUDE.md @@ -27,7 +27,7 @@ Use `pnpm` workspaces and Turborepo. - `packages/cli` is fully disabled for active development. - Do not implement new behavior in `packages/cli` unless explicitly requested. - Frontend is the primary product surface; prioritize `packages/frontend` + supporting `packages/client`/`packages/backend`. -- Workspace `build`, `typecheck`, and `test` intentionally exclude `@openhandoff/cli`. +- Workspace `build`, `typecheck`, and `test` intentionally exclude `@sandbox-agent/factory-cli`. - `pnpm-workspace.yaml` excludes `packages/cli` from workspace package resolution. ## Common Commands @@ -37,8 +37,8 @@ Use `pnpm` workspaces and Turborepo. - Start the full dev stack: `just factory-dev` - Start the local production-build preview stack: `just factory-preview` - Start only the backend locally: `just factory-backend-start` -- Start only the frontend locally: `pnpm --filter @openhandoff/frontend dev` -- Start the frontend against the mock workbench client: `OPENHANDOFF_FRONTEND_CLIENT_MODE=mock pnpm --filter @openhandoff/frontend dev` +- Start only the frontend locally: `pnpm --filter @sandbox-agent/factory-frontend dev` +- Start the frontend against the mock workbench client: `FACTORY_FRONTEND_CLIENT_MODE=mock pnpm --filter @sandbox-agent/factory-frontend dev` - Stop the compose dev stack: `just factory-dev-down` - Tail compose logs: `just factory-dev-logs` - Stop the preview stack: `just factory-preview-down` @@ -85,10 +85,12 @@ For all Rivet/RivetKit implementation: 5. RivetKit is linked via pnpm `link:` protocol to `../rivet/rivetkit-typescript/packages/rivetkit`. Sub-packages (`@rivetkit/sqlite-vfs`, etc.) resolve transitively from the rivet workspace. - Dedicated local checkout for this workspace: `/Users/nathan/conductor/workspaces/handoff/rivet-checkout` - Dev worktree note: when working on RivetKit fixes for this repo, prefer the dedicated local checkout above and link to `../rivet-checkout/rivetkit-typescript/packages/rivetkit`. -6. Before using, build RivetKit in the rivet repo: + - If Docker dev needs a different host path, export `HF_RIVET_CHECKOUT_PATH=/abs/path/to/rivet-checkout` before `docker compose -f factory/compose.dev.yaml up`. +6. Before using a fresh Rivet checkout, generate RivetKit schemas and build RivetKit in the rivet repo: ```bash cd ../rivet-checkout/rivetkit-typescript pnpm install + pnpm --dir packages/rivetkit build:schema pnpm build -F rivetkit ``` @@ -132,7 +134,7 @@ For all Rivet/RivetKit implementation: - Workspace resolution order: `--workspace` flag -> config default -> `"default"`. - `ControlPlaneActor` is replaced by `WorkspaceActor` (workspace coordinator). - Every actor key must be prefixed with workspace namespace (`["ws", workspaceId, ...]`). -- CLI/TUI/GUI must use `@openhandoff/client` (`packages/client`) for backend access; `rivetkit/client` imports are only allowed inside `packages/client`. +- CLI/TUI/GUI must use `@sandbox-agent/factory-client` (`packages/client`) for backend access; `rivetkit/client` imports are only allowed inside `packages/client`. - Do not add custom backend REST endpoints (no `/v1/*` shim layer). - We own the sandbox-agent project; treat sandbox-agent defects as first-party bugs and fix them instead of working around them. - Keep strict single-writer ownership: each table/row has exactly one actor writer. @@ -144,7 +146,7 @@ For all Rivet/RivetKit implementation: - Use create semantics only on explicit provisioning/create paths where creating a new actor instance is intended. - `getOrCreate` is a last resort for create paths when an explicit create API is unavailable; never use it in read/command paths. - For long-lived cross-actor links (for example sandbox/session runtime access), persist actor identity (`actorId`) and keep a fallback lookup path by actor id. -- Docker dev: `compose.dev.yaml` mounts a named volume at `/root/.local/share/openhandoff/repos` to persist backend-managed git clones across restarts. Code must still work if this volume is not present (create directories as needed). +- Docker dev: `compose.dev.yaml` mounts a named volume at `/root/.local/share/sandbox-agent-factory/repos` to persist backend-managed git clones across restarts. Code must still work if this volume is not present (create directories as needed). - RivetKit actor `c.state` is durable, but in Docker it is stored under `/root/.local/share/rivetkit`. If that path is not persisted, actor state-derived indexes (for example, in `project` actor state) can be lost after container recreation even when other data still exists. - Workflow history divergence policy: - Production: never auto-delete actor state to resolve `HistoryDivergedError`; ship explicit workflow migrations (`ctx.removed(...)`, step compatibility). @@ -167,7 +169,7 @@ For all Rivet/RivetKit implementation: ## Config -- Keep config path at `~/.config/openhandoff/config.toml`. +- Keep config path at `~/.config/sandbox-agent-factory/config.toml`. - Evolve properties in place; do not move config location. ## Project Guidance diff --git a/factory/CONTRIBUTING.md b/factory/CONTRIBUTING.md index 759f348..04a9e2d 100644 --- a/factory/CONTRIBUTING.md +++ b/factory/CONTRIBUTING.md @@ -5,8 +5,8 @@ 1. Clone: ```bash -git clone https://github.com/rivet-dev/openhandoff.git -cd openhandoff +git clone https://github.com/rivet-dev/sandbox-agent-factory.git +cd sandbox-agent-factory ``` 2. Install dependencies: @@ -35,7 +35,7 @@ Build local RivetKit before backend changes that depend on Rivet internals: cd ../rivet pnpm build -F rivetkit -cd /path/to/openhandoff +cd /path/to/sandbox-agent-factory just sync-rivetkit ``` diff --git a/factory/Dockerfile b/factory/Dockerfile index 5693650..8d5dfe9 100644 --- a/factory/Dockerfile +++ b/factory/Dockerfile @@ -22,15 +22,15 @@ COPY packages/rivetkit-vendor/sqlite-vfs-win32-x64/package.json packages/rivetki COPY packages/rivetkit-vendor/runner/package.json packages/rivetkit-vendor/runner/package.json COPY packages/rivetkit-vendor/runner-protocol/package.json packages/rivetkit-vendor/runner-protocol/package.json COPY packages/rivetkit-vendor/virtual-websocket/package.json packages/rivetkit-vendor/virtual-websocket/package.json -RUN pnpm fetch --frozen-lockfile --filter @openhandoff/backend... +RUN pnpm fetch --frozen-lockfile --filter @sandbox-agent/factory-backend... FROM base AS build COPY --from=deps /pnpm/store /pnpm/store COPY . . -RUN pnpm install --frozen-lockfile --prefer-offline --filter @openhandoff/backend... -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/backend build -RUN pnpm --filter @openhandoff/backend deploy --prod --legacy /out +RUN pnpm install --frozen-lockfile --prefer-offline --filter @sandbox-agent/factory-backend... +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-backend build +RUN pnpm --filter @sandbox-agent/factory-backend deploy --prod --legacy /out FROM oven/bun:1.2 AS runtime ENV NODE_ENV=production diff --git a/factory/README.md b/factory/README.md index c49f7cb..a385089 100644 --- a/factory/README.md +++ b/factory/README.md @@ -1,9 +1,7 @@ -# OpenHandoff +# Sandbox Agent Factory TypeScript workspace handoff system powered by RivetKit actors, SQLite/Drizzle state, and OpenTUI. -**Documentation**: [openhandoff.dev](https://openhandoff.dev) - ## Quick Install ```bash diff --git a/factory/compose.dev.yaml b/factory/compose.dev.yaml index 80fea12..e4ec7a3 100644 --- a/factory/compose.dev.yaml +++ b/factory/compose.dev.yaml @@ -1,17 +1,17 @@ -name: openhandoff +name: sandbox-agent-factory services: backend: build: context: .. dockerfile: factory/docker/backend.dev.Dockerfile - image: openhandoff-backend-dev + image: sandbox-agent-factory-backend-dev working_dir: /app environment: HF_BACKEND_HOST: "0.0.0.0" HF_BACKEND_PORT: "7741" HF_RIVET_MANAGER_PORT: "8750" - RIVETKIT_STORAGE_PATH: "/root/.local/share/openhandoff/rivetkit" + RIVETKIT_STORAGE_PATH: "/root/.local/share/sandbox-agent-factory/rivetkit" # Pass through credentials needed for agent execution + PR creation in dev/e2e. # Do not hardcode secrets; set these in your environment when starting compose. ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" @@ -32,21 +32,21 @@ services: - "8750:8750" volumes: - "..:/app" - # The linked RivetKit checkout resolves from factory packages to /handoff/rivet-checkout in-container. - - "../../../handoff/rivet-checkout:/handoff/rivet-checkout:ro" + # Override HF_RIVET_CHECKOUT_PATH when the linked Rivet workspace lives outside the default sibling checkout. + - "${HF_RIVET_CHECKOUT_PATH:-../../../handoff/rivet-checkout}:/handoff/rivet-checkout:ro" # Reuse the host Codex auth profile for local sandbox-agent Codex sessions in dev. - "${HOME}/.codex:/root/.codex" # Keep backend dependency installs Linux-native instead of using host node_modules. - - "openhandoff_backend_root_node_modules:/app/node_modules" - - "openhandoff_backend_backend_node_modules:/app/factory/packages/backend/node_modules" - - "openhandoff_backend_shared_node_modules:/app/factory/packages/shared/node_modules" - - "openhandoff_backend_persist_rivet_node_modules:/app/sdks/persist-rivet/node_modules" - - "openhandoff_backend_typescript_node_modules:/app/sdks/typescript/node_modules" - - "openhandoff_backend_pnpm_store:/root/.local/share/pnpm/store" + - "sandbox-agent-factory_backend_root_node_modules:/app/node_modules" + - "sandbox-agent-factory_backend_backend_node_modules:/app/factory/packages/backend/node_modules" + - "sandbox-agent-factory_backend_shared_node_modules:/app/factory/packages/shared/node_modules" + - "sandbox-agent-factory_backend_persist_rivet_node_modules:/app/sdks/persist-rivet/node_modules" + - "sandbox-agent-factory_backend_typescript_node_modules:/app/sdks/typescript/node_modules" + - "sandbox-agent-factory_backend_pnpm_store:/root/.local/share/pnpm/store" # Persist backend-managed local git clones across container restarts. - - "openhandoff_git_repos:/root/.local/share/openhandoff/repos" + - "sandbox-agent-factory_git_repos:/root/.local/share/sandbox-agent-factory/repos" # Persist RivetKit local storage across container restarts. - - "openhandoff_rivetkit_storage:/root/.local/share/openhandoff/rivetkit" + - "sandbox-agent-factory_rivetkit_storage:/root/.local/share/sandbox-agent-factory/rivetkit" frontend: build: @@ -62,29 +62,29 @@ services: - "4173:4173" volumes: - "..:/app" - # Ensure logs in .openhandoff/ persist on the host even if we change source mounts later. - - "./.openhandoff:/app/factory/.openhandoff" - - "../../../handoff/rivet-checkout:/handoff/rivet-checkout:ro" + # Ensure logs in .sandbox-agent-factory/ persist on the host even if we change source mounts later. + - "./.sandbox-agent-factory:/app/factory/.sandbox-agent-factory" + - "${HF_RIVET_CHECKOUT_PATH:-../../../handoff/rivet-checkout}:/handoff/rivet-checkout:ro" # Use Linux-native workspace dependencies inside the container instead of host node_modules. - - "openhandoff_node_modules:/app/node_modules" - - "openhandoff_client_node_modules:/app/factory/packages/client/node_modules" - - "openhandoff_frontend_errors_node_modules:/app/factory/packages/frontend-errors/node_modules" - - "openhandoff_frontend_node_modules:/app/factory/packages/frontend/node_modules" - - "openhandoff_shared_node_modules:/app/factory/packages/shared/node_modules" - - "openhandoff_pnpm_store:/tmp/.local/share/pnpm/store" + - "sandbox-agent-factory_node_modules:/app/node_modules" + - "sandbox-agent-factory_client_node_modules:/app/factory/packages/client/node_modules" + - "sandbox-agent-factory_frontend_errors_node_modules:/app/factory/packages/frontend-errors/node_modules" + - "sandbox-agent-factory_frontend_node_modules:/app/factory/packages/frontend/node_modules" + - "sandbox-agent-factory_shared_node_modules:/app/factory/packages/shared/node_modules" + - "sandbox-agent-factory_pnpm_store:/tmp/.local/share/pnpm/store" volumes: - openhandoff_backend_root_node_modules: {} - openhandoff_backend_backend_node_modules: {} - openhandoff_backend_shared_node_modules: {} - openhandoff_backend_persist_rivet_node_modules: {} - openhandoff_backend_typescript_node_modules: {} - openhandoff_backend_pnpm_store: {} - openhandoff_git_repos: {} - openhandoff_rivetkit_storage: {} - openhandoff_node_modules: {} - openhandoff_client_node_modules: {} - openhandoff_frontend_errors_node_modules: {} - openhandoff_frontend_node_modules: {} - openhandoff_shared_node_modules: {} - openhandoff_pnpm_store: {} + sandbox-agent-factory_backend_root_node_modules: {} + sandbox-agent-factory_backend_backend_node_modules: {} + sandbox-agent-factory_backend_shared_node_modules: {} + sandbox-agent-factory_backend_persist_rivet_node_modules: {} + sandbox-agent-factory_backend_typescript_node_modules: {} + sandbox-agent-factory_backend_pnpm_store: {} + sandbox-agent-factory_git_repos: {} + sandbox-agent-factory_rivetkit_storage: {} + sandbox-agent-factory_node_modules: {} + sandbox-agent-factory_client_node_modules: {} + sandbox-agent-factory_frontend_errors_node_modules: {} + sandbox-agent-factory_frontend_node_modules: {} + sandbox-agent-factory_shared_node_modules: {} + sandbox-agent-factory_pnpm_store: {} diff --git a/factory/compose.preview.yaml b/factory/compose.preview.yaml index 88cdad3..01bbe93 100644 --- a/factory/compose.preview.yaml +++ b/factory/compose.preview.yaml @@ -1,16 +1,16 @@ -name: openhandoff-preview +name: sandbox-agent-factory-preview services: backend: build: context: .. dockerfile: quebec/docker/backend.preview.Dockerfile - image: openhandoff-backend-preview + image: sandbox-agent-factory-backend-preview environment: HF_BACKEND_HOST: "0.0.0.0" HF_BACKEND_PORT: "7841" HF_RIVET_MANAGER_PORT: "8850" - RIVETKIT_STORAGE_PATH: "/root/.local/share/openhandoff/rivetkit" + RIVETKIT_STORAGE_PATH: "/root/.local/share/sandbox-agent-factory/rivetkit" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" CLAUDE_API_KEY: "${CLAUDE_API_KEY:-${ANTHROPIC_API_KEY:-}}" OPENAI_API_KEY: "${OPENAI_API_KEY:-}" @@ -26,19 +26,19 @@ services: - "8850:8850" volumes: - "${HOME}/.codex:/root/.codex" - - "openhandoff_preview_git_repos:/root/.local/share/openhandoff/repos" - - "openhandoff_preview_rivetkit_storage:/root/.local/share/openhandoff/rivetkit" + - "sandbox-agent-factory_preview_git_repos:/root/.local/share/sandbox-agent-factory/repos" + - "sandbox-agent-factory_preview_rivetkit_storage:/root/.local/share/sandbox-agent-factory/rivetkit" frontend: build: context: .. dockerfile: quebec/docker/frontend.preview.Dockerfile - image: openhandoff-frontend-preview + image: sandbox-agent-factory-frontend-preview depends_on: - backend ports: - "4273:4273" volumes: - openhandoff_preview_git_repos: {} - openhandoff_preview_rivetkit_storage: {} + sandbox-agent-factory_preview_git_repos: {} + sandbox-agent-factory_preview_rivetkit_storage: {} diff --git a/factory/docker/backend.dev.Dockerfile b/factory/docker/backend.dev.Dockerfile index a53e018..fb84e70 100644 --- a/factory/docker/backend.dev.Dockerfile +++ b/factory/docker/backend.dev.Dockerfile @@ -39,4 +39,4 @@ ENV SANDBOX_AGENT_BIN="/root/.local/bin/sandbox-agent" WORKDIR /app -CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --force --frozen-lockfile --filter @openhandoff/backend... && exec bun factory/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"] +CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --force --frozen-lockfile --filter @sandbox-agent/factory-backend... && exec bun factory/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"] diff --git a/factory/docker/backend.preview.Dockerfile b/factory/docker/backend.preview.Dockerfile index 3ea5aa8..dbadf36 100644 --- a/factory/docker/backend.preview.Dockerfile +++ b/factory/docker/backend.preview.Dockerfile @@ -42,8 +42,8 @@ COPY quebec /workspace/quebec COPY rivet-checkout /workspace/rivet-checkout RUN pnpm install --frozen-lockfile -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/client build -RUN pnpm --filter @openhandoff/backend build +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-client build +RUN pnpm --filter @sandbox-agent/factory-backend build CMD ["bash", "-lc", "git config --global --add safe.directory /workspace/quebec >/dev/null 2>&1 || true; exec bun packages/backend/dist/index.js start --host 0.0.0.0 --port 7841"] diff --git a/factory/docker/frontend.dev.Dockerfile b/factory/docker/frontend.dev.Dockerfile index 057b88d..eb52d2e 100644 --- a/factory/docker/frontend.dev.Dockerfile +++ b/factory/docker/frontend.dev.Dockerfile @@ -8,4 +8,4 @@ RUN npm install -g pnpm@10.28.2 WORKDIR /app -CMD ["bash", "-lc", "pnpm install --force --frozen-lockfile --filter @openhandoff/frontend... && cd factory/packages/frontend && exec pnpm vite --host 0.0.0.0 --port 4173"] +CMD ["bash", "-lc", "pnpm install --force --frozen-lockfile --filter @sandbox-agent/factory-frontend... && cd factory/packages/frontend && exec pnpm vite --host 0.0.0.0 --port 4173"] diff --git a/factory/docker/frontend.preview.Dockerfile b/factory/docker/frontend.preview.Dockerfile index 7f90b2a..aaf3ae0 100644 --- a/factory/docker/frontend.preview.Dockerfile +++ b/factory/docker/frontend.preview.Dockerfile @@ -10,10 +10,10 @@ COPY quebec /workspace/quebec COPY rivet-checkout /workspace/rivet-checkout RUN pnpm install --frozen-lockfile -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/client build -RUN pnpm --filter @openhandoff/frontend-errors build -RUN pnpm --filter @openhandoff/frontend build +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-client build +RUN pnpm --filter @sandbox-agent/factory-frontend-errors build +RUN pnpm --filter @sandbox-agent/factory-frontend build FROM nginx:1.27-alpine diff --git a/factory/packages/backend/package.json b/factory/packages/backend/package.json index 68c514c..4427b88 100644 --- a/factory/packages/backend/package.json +++ b/factory/packages/backend/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/backend", + "name": "@sandbox-agent/factory-backend", "version": "0.1.0", "private": true, "type": "module", @@ -17,7 +17,7 @@ "@hono/node-server": "^1.19.7", "@hono/node-ws": "^1.3.0", "@iarna/toml": "^2.2.5", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "@sandbox-agent/persist-rivet": "workspace:*", "drizzle-orm": "^0.44.5", "hono": "^4.11.9", diff --git a/factory/packages/backend/src/actors/context.ts b/factory/packages/backend/src/actors/context.ts index 3a7a875..954a22a 100644 --- a/factory/packages/backend/src/actors/context.ts +++ b/factory/packages/backend/src/actors/context.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../driver.js"; import type { NotificationService } from "../notifications/index.js"; import type { ProviderRegistry } from "../providers/index.js"; diff --git a/factory/packages/backend/src/actors/events.ts b/factory/packages/backend/src/actors/events.ts index 8f9ea28..958b105 100644 --- a/factory/packages/backend/src/actors/events.ts +++ b/factory/packages/backend/src/actors/events.ts @@ -1,4 +1,4 @@ -import type { HandoffStatus, ProviderId } from "@openhandoff/shared"; +import type { HandoffStatus, ProviderId } from "@sandbox-agent/factory-shared"; export interface HandoffCreatedEvent { workspaceId: string; diff --git a/factory/packages/backend/src/actors/handles.ts b/factory/packages/backend/src/actors/handles.ts index a05a7fb..8f06f6a 100644 --- a/factory/packages/backend/src/actors/handles.ts +++ b/factory/packages/backend/src/actors/handles.ts @@ -8,7 +8,7 @@ import { sandboxInstanceKey, workspaceKey } from "./keys.js"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; export function actorClient(c: any) { return c.client(); diff --git a/factory/packages/backend/src/actors/handoff-status-sync/index.ts b/factory/packages/backend/src/actors/handoff-status-sync/index.ts index 86c8b3d..296db21 100644 --- a/factory/packages/backend/src/actors/handoff-status-sync/index.ts +++ b/factory/packages/backend/src/actors/handoff-status-sync/index.ts @@ -1,6 +1,6 @@ import { actor, queue } from "rivetkit"; import { workflow } from "rivetkit/workflow"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; import { getHandoff, getSandboxInstance, selfHandoffStatusSync } from "../handles.js"; import { logActorWarning, resolveErrorMessage, resolveErrorStack } from "../logging.js"; import { type PollingControlState, runWorkflowPollingLoop } from "../polling.js"; diff --git a/factory/packages/backend/src/actors/handoff/index.ts b/factory/packages/backend/src/actors/handoff/index.ts index 2ad52f7..4715ad0 100644 --- a/factory/packages/backend/src/actors/handoff/index.ts +++ b/factory/packages/backend/src/actors/handoff/index.ts @@ -10,7 +10,7 @@ import type { HandoffWorkbenchSendMessageInput, HandoffWorkbenchUpdateDraftInput, ProviderId -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { expectQueueResponse } from "../../services/queue.js"; import { selfHandoff } from "../handles.js"; import { handoffDb } from "./db/db.js"; diff --git a/factory/packages/backend/src/actors/handoff/workflow/common.ts b/factory/packages/backend/src/actors/handoff/workflow/common.ts index 45c1df6..f517e11 100644 --- a/factory/packages/backend/src/actors/handoff/workflow/common.ts +++ b/factory/packages/backend/src/actors/handoff/workflow/common.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { eq } from "drizzle-orm"; -import type { HandoffRecord, HandoffStatus } from "@openhandoff/shared"; +import type { HandoffRecord, HandoffStatus } from "@sandbox-agent/factory-shared"; import { getOrCreateWorkspace } from "../../handles.js"; import { handoff as handoffTable, handoffRuntime, handoffSandboxes } from "../db/schema.js"; import { historyKey } from "../../keys.js"; diff --git a/factory/packages/backend/src/actors/handoff/workflow/index.ts b/factory/packages/backend/src/actors/handoff/workflow/index.ts index 7c090f9..a21d32a 100644 --- a/factory/packages/backend/src/actors/handoff/workflow/index.ts +++ b/factory/packages/backend/src/actors/handoff/workflow/index.ts @@ -46,6 +46,8 @@ import { export { HANDOFF_QUEUE_NAMES, handoffWorkflowQueueName } from "./queue.js"; +const INIT_ENSURE_NAME_TIMEOUT_MS = 5 * 60_000; + type HandoffQueueName = (typeof HANDOFF_QUEUE_NAMES)[number]; type WorkflowHandler = (loopCtx: any, msg: { name: HandoffQueueName; body: any; complete: (response: unknown) => Promise }) => Promise; @@ -75,7 +77,11 @@ const commandHandlers: Record = { const body = msg.body; await loopCtx.removed("init-failed", "step"); try { - await loopCtx.step("init-ensure-name", async () => initEnsureNameActivity(loopCtx)); + await loopCtx.step({ + name: "init-ensure-name", + timeout: INIT_ENSURE_NAME_TIMEOUT_MS, + run: async () => initEnsureNameActivity(loopCtx), + }); await loopCtx.step("init-assert-name", async () => initAssertNameActivity(loopCtx)); const sandbox = await loopCtx.step({ diff --git a/factory/packages/backend/src/actors/history/index.ts b/factory/packages/backend/src/actors/history/index.ts index 15e7ca5..e051fc7 100644 --- a/factory/packages/backend/src/actors/history/index.ts +++ b/factory/packages/backend/src/actors/history/index.ts @@ -2,7 +2,7 @@ import { and, desc, eq } from "drizzle-orm"; import { actor, queue } from "rivetkit"; import { Loop, workflow } from "rivetkit/workflow"; -import type { HistoryEvent } from "@openhandoff/shared"; +import type { HistoryEvent } from "@sandbox-agent/factory-shared"; import { selfHistory } from "../handles.js"; import { historyDb } from "./db/db.js"; import { events } from "./db/schema.js"; diff --git a/factory/packages/backend/src/actors/logging.ts b/factory/packages/backend/src/actors/logging.ts index ffc45ab..8f43f72 100644 --- a/factory/packages/backend/src/actors/logging.ts +++ b/factory/packages/backend/src/actors/logging.ts @@ -27,5 +27,5 @@ export function logActorWarning( ...(context ?? {}) }; // eslint-disable-next-line no-console - console.warn("[openhandoff][actor:warn]", payload); + console.warn("[factory][actor:warn]", payload); } diff --git a/factory/packages/backend/src/actors/project/actions.ts b/factory/packages/backend/src/actors/project/actions.ts index d0fc978..fab3dfc 100644 --- a/factory/packages/backend/src/actors/project/actions.ts +++ b/factory/packages/backend/src/actors/project/actions.ts @@ -10,7 +10,7 @@ import type { RepoOverview, RepoStackAction, RepoStackActionResult -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { getActorRuntimeContext } from "../context.js"; import { getHandoff, @@ -21,7 +21,7 @@ import { selfProject } from "../handles.js"; import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js"; -import { openhandoffRepoClonePath } from "../../services/openhandoff-paths.js"; +import { factoryRepoClonePath } from "../../services/factory-paths.js"; import { expectQueueResponse } from "../../services/queue.js"; import { withRepoGitLock } from "../../services/repo-git-lock.js"; import { branches, handoffIndex, prCache, repoMeta } from "./db/schema.js"; @@ -125,7 +125,7 @@ export function projectWorkflowQueueName(name: ProjectQueueName): ProjectQueueNa async function ensureLocalClone(c: any, remoteUrl: string): Promise { const { config, driver } = getActorRuntimeContext(); - const localPath = openhandoffRepoClonePath(config, c.state.workspaceId, c.state.repoId); + const localPath = factoryRepoClonePath(config, c.state.workspaceId, c.state.repoId); await driver.git.ensureCloned(remoteUrl, localPath); c.state.localPath = localPath; return localPath; diff --git a/factory/packages/backend/src/actors/sandbox-instance/index.ts b/factory/packages/backend/src/actors/sandbox-instance/index.ts index e20b86a..5a53647 100644 --- a/factory/packages/backend/src/actors/sandbox-instance/index.ts +++ b/factory/packages/backend/src/actors/sandbox-instance/index.ts @@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises"; import { eq } from "drizzle-orm"; import { actor, queue } from "rivetkit"; import { Loop, workflow } from "rivetkit/workflow"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; import type { SessionEvent, SessionRecord } from "sandbox-agent"; import { sandboxInstanceDb } from "./db/db.js"; import { sandboxInstance as sandboxInstanceTable } from "./db/schema.js"; diff --git a/factory/packages/backend/src/actors/workspace/actions.ts b/factory/packages/backend/src/actors/workspace/actions.ts index 93acf16..7c41ffe 100644 --- a/factory/packages/backend/src/actors/workspace/actions.ts +++ b/factory/packages/backend/src/actors/workspace/actions.ts @@ -27,7 +27,7 @@ import type { RepoRecord, SwitchResult, WorkspaceUseInput -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { getActorRuntimeContext } from "../context.js"; import { getHandoff, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js"; import { logActorWarning, resolveErrorMessage } from "../logging.js"; diff --git a/factory/packages/backend/src/config/backend.ts b/factory/packages/backend/src/config/backend.ts index 66ac3f1..4c1bd18 100644 --- a/factory/packages/backend/src/config/backend.ts +++ b/factory/packages/backend/src/config/backend.ts @@ -2,9 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; import { homedir } from "node:os"; import * as toml from "@iarna/toml"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; -export const CONFIG_PATH = `${homedir()}/.config/openhandoff/config.toml`; +export const CONFIG_PATH = `${homedir()}/.config/sandbox-agent-factory/config.toml`; export function loadConfig(path = CONFIG_PATH): AppConfig { if (!existsSync(path)) { diff --git a/factory/packages/backend/src/config/workspace.ts b/factory/packages/backend/src/config/workspace.ts index a7b4010..4937bdc 100644 --- a/factory/packages/backend/src/config/workspace.ts +++ b/factory/packages/backend/src/config/workspace.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; export function defaultWorkspace(config: AppConfig): string { const ws = config.workspace.default.trim(); diff --git a/factory/packages/backend/src/db/actor-sqlite.ts b/factory/packages/backend/src/db/actor-sqlite.ts index fdae16f..76d688a 100644 --- a/factory/packages/backend/src/db/actor-sqlite.ts +++ b/factory/packages/backend/src/db/actor-sqlite.ts @@ -29,7 +29,7 @@ export interface ActorSqliteDbOptions> { /** * Override base directory for per-actor SQLite files. * - * Default: `/.openhandoff/backend/sqlite` + * Default: `/.sandbox-agent-factory/backend/sqlite` */ baseDir?: string; } @@ -53,7 +53,7 @@ export function actorSqliteDb>( }) as unknown as DatabaseProvider; } - const baseDir = options.baseDir ?? join(process.cwd(), ".openhandoff", "backend", "sqlite"); + const baseDir = options.baseDir ?? join(process.cwd(), ".sandbox-agent-factory", "backend", "sqlite"); const migrationsFolder = fileURLToPath(options.migrationsFolderUrl); return { diff --git a/factory/packages/backend/src/integrations/git/index.ts b/factory/packages/backend/src/integrations/git/index.ts index a27d469..a617d14 100644 --- a/factory/packages/backend/src/integrations/git/index.ts +++ b/factory/packages/backend/src/integrations/git/index.ts @@ -28,7 +28,7 @@ function ensureAskpassScript(): string { return cachedAskpassPath; } - const dir = mkdtempSync(resolve(tmpdir(), "openhandoff-git-askpass-")); + const dir = mkdtempSync(resolve(tmpdir(), "factory-git-askpass-")); const path = resolve(dir, "askpass.sh"); // Git invokes $GIT_ASKPASS with the prompt string as argv[1]. Provide both username and password. diff --git a/factory/packages/backend/src/integrations/sandbox-agent/client.ts b/factory/packages/backend/src/integrations/sandbox-agent/client.ts index 1dc28e7..27c5824 100644 --- a/factory/packages/backend/src/integrations/sandbox-agent/client.ts +++ b/factory/packages/backend/src/integrations/sandbox-agent/client.ts @@ -1,4 +1,4 @@ -import type { AgentType } from "@openhandoff/shared"; +import type { AgentType } from "@sandbox-agent/factory-shared"; import type { ListEventsRequest, ListPage, @@ -144,7 +144,7 @@ export class SandboxAgentClient { const modeId = modeIdForAgent(normalized.agent ?? this.agent); // Codex defaults to a restrictive "read-only" preset in some environments. - // For OpenHandoff automation we need to allow edits + command execution + network + // For Sandbox Agent Factory automation we need to allow edits + command execution + network // access (git push / PR creation). Use full-access where supported. // // If the agent doesn't support session modes, ignore. diff --git a/factory/packages/backend/src/providers/daytona/index.ts b/factory/packages/backend/src/providers/daytona/index.ts index 97d6aee..471552a 100644 --- a/factory/packages/backend/src/providers/daytona/index.ts +++ b/factory/packages/backend/src/providers/daytona/index.ts @@ -205,11 +205,11 @@ export class DaytonaProvider implements SandboxProvider { image: this.buildSnapshotImage(), envVars: this.buildEnvVars(), labels: { - "openhandoff.workspace": req.workspaceId, - "openhandoff.handoff": req.handoffId, - "openhandoff.repo_id": req.repoId, - "openhandoff.repo_remote": req.repoRemote, - "openhandoff.branch": req.branchName, + "factory.workspace": req.workspaceId, + "factory.handoff": req.handoffId, + "factory.repo_id": req.repoId, + "factory.repo_remote": req.repoRemote, + "factory.branch": req.branchName, }, autoStopInterval: this.config.autoStopInterval, }) @@ -220,7 +220,7 @@ export class DaytonaProvider implements SandboxProvider { state: sandbox.state ?? null }); - const repoDir = `/home/daytona/openhandoff/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`; + const repoDir = `/home/daytona/sandbox-agent-factory/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`; // Prepare a working directory for the agent. This must succeed for the handoff to work. const installStartedAt = Date.now(); @@ -258,8 +258,8 @@ export class DaytonaProvider implements SandboxProvider { `git fetch origin --prune`, // The handoff branch may not exist remotely yet (agent push creates it). Base off current branch (default branch). `if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`, - `git config user.email "openhandoff@local" >/dev/null 2>&1 || true`, - `git config user.name "OpenHandoff" >/dev/null 2>&1 || true`, + `git config user.email "factory@local" >/dev/null 2>&1 || true`, + `git config user.name "Sandbox Agent Factory" >/dev/null 2>&1 || true`, ].join("; ") )}` ].join(" "), @@ -294,12 +294,12 @@ export class DaytonaProvider implements SandboxProvider { client.getSandbox(req.sandboxId) ); const labels = info.labels ?? {}; - const workspaceId = labels["openhandoff.workspace"] ?? req.workspaceId; - const repoId = labels["openhandoff.repo_id"] ?? ""; - const handoffId = labels["openhandoff.handoff"] ?? ""; + const workspaceId = labels["factory.workspace"] ?? req.workspaceId; + const repoId = labels["factory.repo_id"] ?? ""; + const handoffId = labels["factory.handoff"] ?? ""; const cwd = repoId && handoffId - ? `/home/daytona/openhandoff/${workspaceId}/${repoId}/${handoffId}/repo` + ? `/home/daytona/sandbox-agent-factory/${workspaceId}/${repoId}/${handoffId}/repo` : null; return { diff --git a/factory/packages/backend/src/providers/index.ts b/factory/packages/backend/src/providers/index.ts index 1410f94..ad0f725 100644 --- a/factory/packages/backend/src/providers/index.ts +++ b/factory/packages/backend/src/providers/index.ts @@ -1,5 +1,5 @@ -import type { ProviderId } from "@openhandoff/shared"; -import type { AppConfig } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../driver.js"; import { DaytonaProvider } from "./daytona/index.js"; import { LocalProvider } from "./local/index.js"; diff --git a/factory/packages/backend/src/providers/local/index.ts b/factory/packages/backend/src/providers/local/index.ts index 3317c24..c869f6a 100644 --- a/factory/packages/backend/src/providers/local/index.ts +++ b/factory/packages/backend/src/providers/local/index.ts @@ -77,7 +77,7 @@ export class LocalProvider implements SandboxProvider { private rootDir(): string { return expandHome( - this.config.rootDir?.trim() || "~/.local/share/openhandoff/local-sandboxes", + this.config.rootDir?.trim() || "~/.local/share/sandbox-agent-factory/local-sandboxes", ); } diff --git a/factory/packages/backend/src/providers/provider-api/index.ts b/factory/packages/backend/src/providers/provider-api/index.ts index 5735ec4..67a9af1 100644 --- a/factory/packages/backend/src/providers/provider-api/index.ts +++ b/factory/packages/backend/src/providers/provider-api/index.ts @@ -1,4 +1,4 @@ -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; export interface ProviderCapabilities { remote: boolean; diff --git a/factory/packages/backend/src/services/openhandoff-paths.ts b/factory/packages/backend/src/services/factory-paths.ts similarity index 65% rename from factory/packages/backend/src/services/openhandoff-paths.ts rename to factory/packages/backend/src/services/factory-paths.ts index 79ffca9..25d41ef 100644 --- a/factory/packages/backend/src/services/openhandoff-paths.ts +++ b/factory/packages/backend/src/services/factory-paths.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; @@ -9,17 +9,17 @@ function expandPath(input: string): string { return input; } -export function openhandoffDataDir(config: AppConfig): string { +export function factoryDataDir(config: AppConfig): string { // Keep data collocated with the backend DB by default. const dbPath = expandPath(config.backend.dbPath); return resolve(dirname(dbPath)); } -export function openhandoffRepoClonePath( +export function factoryRepoClonePath( config: AppConfig, workspaceId: string, repoId: string ): string { - return resolve(join(openhandoffDataDir(config), "repos", workspaceId, repoId)); + return resolve(join(factoryDataDir(config), "repos", workspaceId, repoId)); } diff --git a/factory/packages/backend/test/daytona-provider.test.ts b/factory/packages/backend/test/daytona-provider.test.ts index 27c9878..025f0f0 100644 --- a/factory/packages/backend/test/daytona-provider.test.ts +++ b/factory/packages/backend/test/daytona-provider.test.ts @@ -12,7 +12,7 @@ class RecordingDaytonaClient implements DaytonaClientLike { return { id: "sandbox-1", state: "started", - snapshot: "snapshot-openhandoff", + snapshot: "snapshot-factory", labels: {}, }; } @@ -21,7 +21,7 @@ class RecordingDaytonaClient implements DaytonaClientLike { return { id: sandboxId, state: "started", - snapshot: "snapshot-openhandoff", + snapshot: "snapshot-factory", labels: {}, }; } @@ -92,9 +92,9 @@ describe("daytona provider snapshot image behavior", () => { expect(commands).toContain("GIT_TERMINAL_PROMPT=0"); expect(commands).toContain("GIT_ASKPASS=/bin/echo"); - expect(handle.metadata.snapshot).toBe("snapshot-openhandoff"); + expect(handle.metadata.snapshot).toBe("snapshot-factory"); expect(handle.metadata.image).toBe("ubuntu:24.04"); - expect(handle.metadata.cwd).toBe("/home/daytona/openhandoff/default/repo-1/handoff-1/repo"); + expect(handle.metadata.cwd).toBe("/home/daytona/sandbox-agent-factory/default/repo-1/handoff-1/repo"); expect(client.executedCommands.length).toBeGreaterThan(0); }); diff --git a/factory/packages/backend/test/git-validate-remote.test.ts b/factory/packages/backend/test/git-validate-remote.test.ts index ea15ac7..4cea828 100644 --- a/factory/packages/backend/test/git-validate-remote.test.ts +++ b/factory/packages/backend/test/git-validate-remote.test.ts @@ -27,7 +27,7 @@ describe("validateRemote", () => { mkdirSync(brokenRepoDir, { recursive: true }); writeFileSync(resolve(brokenRepoDir, ".git"), "gitdir: /definitely/missing/worktree\n", "utf8"); await execFileAsync("git", ["init", remoteRepoDir]); - await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.name", "OpenHandoff Test"]); + await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.name", "Factory Test"]); await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.email", "test@example.com"]); writeFileSync(resolve(remoteRepoDir, "README.md"), "# test\n", "utf8"); await execFileAsync("git", ["-C", remoteRepoDir, "add", "README.md"]); diff --git a/factory/packages/backend/test/helpers/test-context.ts b/factory/packages/backend/test/helpers/test-context.ts index d779915..b163905 100644 --- a/factory/packages/backend/test/helpers/test-context.ts +++ b/factory/packages/backend/test/helpers/test-context.ts @@ -1,6 +1,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../../src/driver.js"; import { initActorRuntimeContext } from "../../src/actors/context.js"; import { createProviderRegistry } from "../../src/providers/index.js"; diff --git a/factory/packages/backend/test/providers.test.ts b/factory/packages/backend/test/providers.test.ts index 6e3cfb2..a86f7d1 100644 --- a/factory/packages/backend/test/providers.test.ts +++ b/factory/packages/backend/test/providers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import { createProviderRegistry } from "../src/providers/index.js"; function makeConfig(): AppConfig { @@ -10,7 +10,7 @@ function makeConfig(): AppConfig { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/backend/test/repo-normalize.test.ts b/factory/packages/backend/test/repo-normalize.test.ts index 593e26b..5314d95 100644 --- a/factory/packages/backend/test/repo-normalize.test.ts +++ b/factory/packages/backend/test/repo-normalize.test.ts @@ -3,41 +3,41 @@ import { normalizeRemoteUrl, repoIdFromRemote } from "../src/services/repo.js"; describe("normalizeRemoteUrl", () => { test("accepts GitHub shorthand owner/repo", () => { - expect(normalizeRemoteUrl("rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("accepts github.com/owner/repo without scheme", () => { - expect(normalizeRemoteUrl("github.com/rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("github.com/rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("canonicalizes GitHub repo URLs without .git", () => { - expect(normalizeRemoteUrl("https://github.com/rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("https://github.com/rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("canonicalizes GitHub non-clone URLs (e.g. /tree/main)", () => { - expect(normalizeRemoteUrl("https://github.com/rivet-dev/openhandoff/tree/main")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("https://github.com/rivet-dev/sandbox-agent-factory/tree/main")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("does not rewrite scp-style ssh remotes", () => { - expect(normalizeRemoteUrl("git@github.com:rivet-dev/openhandoff.git")).toBe( - "git@github.com:rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("git@github.com:rivet-dev/sandbox-agent-factory.git")).toBe( + "git@github.com:rivet-dev/sandbox-agent-factory.git" ); }); }); describe("repoIdFromRemote", () => { test("repoId is stable across equivalent GitHub inputs", () => { - const a = repoIdFromRemote("rivet-dev/openhandoff"); - const b = repoIdFromRemote("https://github.com/rivet-dev/openhandoff.git"); - const c = repoIdFromRemote("https://github.com/rivet-dev/openhandoff/tree/main"); + const a = repoIdFromRemote("rivet-dev/sandbox-agent-factory"); + const b = repoIdFromRemote("https://github.com/rivet-dev/sandbox-agent-factory.git"); + const c = repoIdFromRemote("https://github.com/rivet-dev/sandbox-agent-factory/tree/main"); expect(a).toBe(b); expect(b).toBe(c); }); diff --git a/factory/packages/backend/test/workspace-isolation.test.ts b/factory/packages/backend/test/workspace-isolation.test.ts index ef31a40..bf3a22b 100644 --- a/factory/packages/backend/test/workspace-isolation.test.ts +++ b/factory/packages/backend/test/workspace-isolation.test.ts @@ -17,7 +17,7 @@ function createRepo(): { repoPath: string } { const repoPath = mkdtempSync(join(tmpdir(), "hf-isolation-repo-")); execFileSync("git", ["init"], { cwd: repoPath }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoPath }); - execFileSync("git", ["config", "user.name", "OpenHandoff Test"], { cwd: repoPath }); + execFileSync("git", ["config", "user.name", "Factory Test"], { cwd: repoPath }); writeFileSync(join(repoPath, "README.md"), "hello\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoPath }); execFileSync("git", ["commit", "-m", "init"], { cwd: repoPath }); diff --git a/factory/packages/backend/tmp-decode-actors.mjs b/factory/packages/backend/tmp-decode-actors.mjs index a25790b..5f9c36c 100644 --- a/factory/packages/backend/tmp-decode-actors.mjs +++ b/factory/packages/backend/tmp-decode-actors.mjs @@ -20,7 +20,7 @@ function locationToNames(entry, names) { } for (const t of targets) { - const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${t.actorId}.db`, { readonly: true }); + const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${t.actorId}.db`, { readonly: true }); const token = new TextDecoder().decode(db.query("SELECT value FROM kv WHERE hex(key)=?").get("03").value); await new Promise((resolve, reject) => { diff --git a/factory/packages/backend/tmp-dump-wfkeys.mjs b/factory/packages/backend/tmp-dump-wfkeys.mjs index 41df274..b38b6bc 100644 --- a/factory/packages/backend/tmp-dump-wfkeys.mjs +++ b/factory/packages/backend/tmp-dump-wfkeys.mjs @@ -1,6 +1,6 @@ import { Database } from "bun:sqlite"; -const db = new Database("/root/.local/share/openhandoff/rivetkit/databases/2e443238457137bf.db", { readonly: true }); +const db = new Database("/root/.local/share/sandbox-agent-factory/rivetkit/databases/2e443238457137bf.db", { readonly: true }); const rows = db.query("SELECT hex(key) as k, value as v FROM kv WHERE hex(key) LIKE ? ORDER BY key").all("07%"); const out = rows.map((r) => { const bytes = new Uint8Array(r.v); diff --git a/factory/packages/backend/tmp-inspect-deep.mjs b/factory/packages/backend/tmp-inspect-deep.mjs index fa5f8db..d2541b0 100644 --- a/factory/packages/backend/tmp-inspect-deep.mjs +++ b/factory/packages/backend/tmp-inspect-deep.mjs @@ -9,7 +9,7 @@ import { decodeReadRangeWire } from "/rivet-handoff-fixes/rivetkit-typescript/pa import { readRangeWireToOtlp } from "/rivet-handoff-fixes/rivetkit-typescript/packages/traces/src/read-range.ts"; const actorId = "2e443238457137bf"; -const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`, { readonly: true }); +const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`, { readonly: true }); const row = db.query("SELECT value FROM kv WHERE hex(key)=?").get("03"); const token = new TextDecoder().decode(row.value); diff --git a/factory/packages/backend/tmp-inspect-stuck.mjs b/factory/packages/backend/tmp-inspect-stuck.mjs index 7bb8c08..e219436 100644 --- a/factory/packages/backend/tmp-inspect-stuck.mjs +++ b/factory/packages/backend/tmp-inspect-stuck.mjs @@ -14,7 +14,7 @@ function decodeAscii(u8) { } for (const actorId of actorIds) { - const dbPath = `/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`; + const dbPath = `/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`; const db = new Database(dbPath, { readonly: true }); const wfStateRow = db.query("SELECT value FROM kv WHERE hex(key)=?").get("0715041501"); diff --git a/factory/packages/backend/tmp-inspect-workflow.mjs b/factory/packages/backend/tmp-inspect-workflow.mjs index 3bc9355..af76bf8 100644 --- a/factory/packages/backend/tmp-inspect-workflow.mjs +++ b/factory/packages/backend/tmp-inspect-workflow.mjs @@ -3,7 +3,7 @@ import { TO_CLIENT_VERSIONED, decodeWorkflowHistoryTransport } from "rivetkit/in import util from "node:util"; const actorId = "2e443238457137bf"; -const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`, { readonly: true }); +const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`, { readonly: true }); const row = db.query("SELECT value FROM kv WHERE hex(key) = ?").get("03"); const token = new TextDecoder().decode(row.value); diff --git a/factory/packages/cli/package.json b/factory/packages/cli/package.json index e6ff8f4..87ef87c 100644 --- a/factory/packages/cli/package.json +++ b/factory/packages/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/cli", + "name": "@sandbox-agent/factory-cli", "version": "0.1.0", "private": true, "type": "module", @@ -16,8 +16,8 @@ "dependencies": { "@iarna/toml": "^2.2.5", "@opentui/core": "^0.1.77", - "@openhandoff/client": "workspace:*", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-client": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "zod": "^4.1.5" }, "devDependencies": { diff --git a/factory/packages/cli/src/backend/manager.ts b/factory/packages/cli/src/backend/manager.ts index 0ae01cb..0bd800b 100644 --- a/factory/packages/cli/src/backend/manager.ts +++ b/factory/packages/cli/src/backend/manager.ts @@ -11,8 +11,8 @@ import { import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { checkBackendHealth } from "@openhandoff/client"; -import type { AppConfig } from "@openhandoff/shared"; +import { checkBackendHealth } from "@sandbox-agent/factory-client"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import { CLI_BUILD_ID } from "../build-id.js"; const HEALTH_TIMEOUT_MS = 1_500; @@ -39,10 +39,10 @@ function backendStateDir(): string { const xdgDataHome = process.env.XDG_DATA_HOME?.trim(); if (xdgDataHome) { - return join(xdgDataHome, "openhandoff", "backend"); + return join(xdgDataHome, "sandbox-agent-factory", "backend"); } - return join(homedir(), ".local", "share", "openhandoff", "backend"); + return join(homedir(), ".local", "share", "sandbox-agent-factory", "backend"); } function backendPidPath(host: string, port: number): string { @@ -214,7 +214,7 @@ function resolveLaunchSpec(host: string, port: number): LaunchSpec { command: "pnpm", args: [ "--filter", - "@openhandoff/backend", + "@sandbox-agent/factory-backend", "exec", "bun", "src/index.ts", diff --git a/factory/packages/cli/src/index.ts b/factory/packages/cli/src/index.ts index 9d764ba..c4dbb62 100644 --- a/factory/packages/cli/src/index.ts +++ b/factory/packages/cli/src/index.ts @@ -2,14 +2,14 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; -import { AgentTypeSchema, CreateHandoffInputSchema, type HandoffRecord } from "@openhandoff/shared"; +import { AgentTypeSchema, CreateHandoffInputSchema, type HandoffRecord } from "@sandbox-agent/factory-shared"; import { readBackendMetadata, createBackendClientFromConfig, formatRelativeAge, groupHandoffStatus, summarizeHandoffs -} from "@openhandoff/client"; +} from "@sandbox-agent/factory-client"; import { ensureBackendRunning, getBackendStatus, diff --git a/factory/packages/cli/src/theme.ts b/factory/packages/cli/src/theme.ts index 5c6a917..32232aa 100644 --- a/factory/packages/cli/src/theme.ts +++ b/factory/packages/cli/src/theme.ts @@ -3,7 +3,7 @@ import { homedir } from "node:os"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { cwd } from "node:process"; import * as toml from "@iarna/toml"; -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import opencodeThemePackJson from "./themes/opencode-pack.json" with { type: "json" }; export type ThemeMode = "dark" | "light"; @@ -101,7 +101,7 @@ export function resolveTuiTheme(config: AppConfig, baseDir = cwd()): TuiThemeRes return { theme: candidate.theme, name: candidate.name, - source: "openhandoff config", + source: "factory config", mode }; } diff --git a/factory/packages/cli/src/tui.ts b/factory/packages/cli/src/tui.ts index 458ede3..b08bb81 100644 --- a/factory/packages/cli/src/tui.ts +++ b/factory/packages/cli/src/tui.ts @@ -1,11 +1,11 @@ -import type { AppConfig, HandoffRecord } from "@openhandoff/shared"; +import type { AppConfig, HandoffRecord } from "@sandbox-agent/factory-shared"; import { spawnSync } from "node:child_process"; import { createBackendClientFromConfig, filterHandoffs, formatRelativeAge, groupHandoffStatus -} from "@openhandoff/client"; +} from "@sandbox-agent/factory-client"; import { CLI_BUILD_ID } from "./build-id.js"; import { resolveTuiTheme, type TuiTheme } from "./theme.js"; @@ -338,7 +338,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise { }); import { ensureBackendRunning, parseBackendPort } from "../src/backend/manager.js"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; function backendStateFile(baseDir: string, host: string, port: number, suffix: string): string { const sanitized = host @@ -62,7 +62,7 @@ describe("backend manager", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/cli/test/theme.test.ts b/factory/packages/cli/test/theme.test.ts index 6ccd902..608426d 100644 --- a/factory/packages/cli/test/theme.test.ts +++ b/factory/packages/cli/test/theme.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import { resolveTuiTheme } from "../src/theme.js"; function withEnv(key: string, value: string | undefined): void { @@ -25,7 +25,7 @@ describe("resolveTuiTheme", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, @@ -98,7 +98,7 @@ describe("resolveTuiTheme", () => { expect(resolution.theme.background).toBe("#0a0a0a"); }); - it("prefers explicit openhandoff theme override from config", () => { + it("prefers explicit factory theme override from config", () => { tempDir = mkdtempSync(join(tmpdir(), "hf-theme-test-")); withEnv("XDG_STATE_HOME", join(tempDir, "state")); withEnv("XDG_CONFIG_HOME", join(tempDir, "config")); @@ -107,6 +107,6 @@ describe("resolveTuiTheme", () => { const resolution = resolveTuiTheme(config, tempDir); expect(resolution.name).toBe("opencode-default"); - expect(resolution.source).toBe("openhandoff config"); + expect(resolution.source).toBe("factory config"); }); }); diff --git a/factory/packages/cli/test/tui-format.test.ts b/factory/packages/cli/test/tui-format.test.ts index e7821f9..79020e6 100644 --- a/factory/packages/cli/test/tui-format.test.ts +++ b/factory/packages/cli/test/tui-format.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord } from "@openhandoff/shared"; -import { filterHandoffs, fuzzyMatch } from "@openhandoff/client"; +import type { HandoffRecord } from "@sandbox-agent/factory-shared"; +import { filterHandoffs, fuzzyMatch } from "@sandbox-agent/factory-client"; import { formatRows } from "../src/tui.js"; const sample: HandoffRecord = { diff --git a/factory/packages/cli/test/workspace-config.test.ts b/factory/packages/cli/test/workspace-config.test.ts index 0666984..86cdc42 100644 --- a/factory/packages/cli/test/workspace-config.test.ts +++ b/factory/packages/cli/test/workspace-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ConfigSchema } from "@openhandoff/shared"; +import { ConfigSchema } from "@sandbox-agent/factory-shared"; import { resolveWorkspace } from "../src/workspace/config.js"; describe("cli workspace resolution", () => { @@ -11,7 +11,7 @@ describe("cli workspace resolution", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/client/package.json b/factory/packages/client/package.json index ebb82f8..93374e4 100644 --- a/factory/packages/client/package.json +++ b/factory/packages/client/package.json @@ -1,12 +1,43 @@ { - "name": "@openhandoff/client", + "name": "@sandbox-agent/factory-client", "version": "0.1.0", "private": true, "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./backend": { + "types": "./dist/backend.d.ts", + "import": "./dist/backend.js" + }, + "./workbench": { + "types": "./dist/workbench.d.ts", + "import": "./dist/workbench.js" + }, + "./view-model": { + "types": "./dist/view-model.d.ts", + "import": "./dist/view-model.js" + } + }, + "typesVersions": { + "*": { + "backend": [ + "dist/backend.d.ts" + ], + "view-model": [ + "dist/view-model.d.ts" + ], + "workbench": [ + "dist/workbench.d.ts" + ] + } + }, "scripts": { - "build": "tsup src/index.ts --format esm --dts", + "build": "tsup src/index.ts src/backend.ts src/workbench.ts src/view-model.ts --format esm --dts", "typecheck": "tsc --noEmit", "test": "vitest run", "test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts", @@ -14,7 +45,7 @@ "test:e2e:workbench-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workbench-load-e2e.test.ts" }, "dependencies": { - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "rivetkit": "link:../../../../../handoff/rivet-checkout/rivetkit-typescript/packages/rivetkit" }, "devDependencies": { diff --git a/factory/packages/client/src/backend-client.ts b/factory/packages/client/src/backend-client.ts index 86288fb..e071546 100644 --- a/factory/packages/client/src/backend-client.ts +++ b/factory/packages/client/src/backend-client.ts @@ -26,7 +26,7 @@ import type { RepoStackActionResult, RepoRecord, SwitchResult -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { sandboxInstanceKey, workspaceKey } from "./keys.js"; export type HandoffAction = "push" | "sync" | "merge" | "archive" | "kill"; diff --git a/factory/packages/client/src/backend.ts b/factory/packages/client/src/backend.ts new file mode 100644 index 0000000..238a032 --- /dev/null +++ b/factory/packages/client/src/backend.ts @@ -0,0 +1 @@ +export * from "./backend-client.js"; diff --git a/factory/packages/client/src/mock/workbench-client.ts b/factory/packages/client/src/mock/workbench-client.ts index b2738dd..1ee27f7 100644 --- a/factory/packages/client/src/mock/workbench-client.ts +++ b/factory/packages/client/src/mock/workbench-client.ts @@ -26,7 +26,7 @@ import type { WorkbenchAgentTab as AgentTab, WorkbenchHandoff as Handoff, WorkbenchTranscriptEvent as TranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { HandoffWorkbenchClient } from "../workbench-client.js"; function buildTranscriptEvent(params: { @@ -48,10 +48,14 @@ function buildTranscriptEvent(params: { } class MockWorkbenchStore implements HandoffWorkbenchClient { - private snapshot = buildInitialMockLayoutViewModel(); + private snapshot: HandoffWorkbenchSnapshot; private listeners = new Set<() => void>(); private pendingTimers = new Map>(); + constructor(workspaceId: string) { + this.snapshot = buildInitialMockLayoutViewModel(workspaceId); + } + getSnapshot(): HandoffWorkbenchSnapshot { return this.snapshot; } @@ -103,6 +107,17 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { ...current, handoffs: [nextHandoff, ...current.handoffs], })); + + const task = input.task.trim(); + if (task) { + await this.sendMessage({ + handoffId: id, + tabId, + text: task, + attachments: [], + }); + } + return { handoffId: id, tabId }; } @@ -149,6 +164,13 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { })); } + async pushHandoff(input: HandoffWorkbenchSelectInput): Promise { + this.updateHandoff(input.handoffId, (handoff) => ({ + ...handoff, + updatedAtMs: nowMs(), + })); + } + async revertFile(input: HandoffWorkbenchDiffInput): Promise { this.updateHandoff(input.handoffId, (handoff) => { const file = handoff.fileChanges.find((entry) => entry.path === input.path); @@ -195,8 +217,11 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { this.updateHandoff(input.handoffId, (currentHandoff) => { const isFirstOnHandoff = currentHandoff.status === "new"; - const newTitle = isFirstOnHandoff ? (text.length > 50 ? `${text.slice(0, 47)}...` : text) : currentHandoff.title; - const newBranch = isFirstOnHandoff ? `feat/${slugify(newTitle)}` : currentHandoff.branch; + const synthesizedTitle = text.length > 50 ? `${text.slice(0, 47)}...` : text; + const newTitle = + isFirstOnHandoff && currentHandoff.title === "New Handoff" ? synthesizedTitle : currentHandoff.title; + const newBranch = + isFirstOnHandoff && !currentHandoff.branch ? `feat/${slugify(synthesizedTitle)}` : currentHandoff.branch; const userMessageLines = [text, ...input.attachments.map((attachment) => `@ ${attachment.filePath}:${attachment.lineNumber}`)]; const userEvent = buildTranscriptEvent({ sessionId: input.tabId, @@ -435,11 +460,13 @@ function candidateEventIndex(handoff: Handoff, tabId: string): number { return (tab?.transcript.length ?? 0) + 1; } -let sharedMockWorkbenchClient: HandoffWorkbenchClient | null = null; +const mockWorkbenchClients = new Map(); -export function getSharedMockWorkbenchClient(): HandoffWorkbenchClient { - if (!sharedMockWorkbenchClient) { - sharedMockWorkbenchClient = new MockWorkbenchStore(); +export function getMockWorkbenchClient(workspaceId = "default"): HandoffWorkbenchClient { + let client = mockWorkbenchClients.get(workspaceId); + if (!client) { + client = new MockWorkbenchStore(workspaceId); + mockWorkbenchClients.set(workspaceId, client); } - return sharedMockWorkbenchClient; + return client; } diff --git a/factory/packages/client/src/remote/workbench-client.ts b/factory/packages/client/src/remote/workbench-client.ts index 720613b..af022f4 100644 --- a/factory/packages/client/src/remote/workbench-client.ts +++ b/factory/packages/client/src/remote/workbench-client.ts @@ -12,7 +12,7 @@ import type { HandoffWorkbenchSnapshot, HandoffWorkbenchTabInput, HandoffWorkbenchUpdateDraftInput, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { BackendClient } from "../backend-client.js"; import { groupWorkbenchProjects } from "../workbench-model.js"; import type { HandoffWorkbenchClient } from "../workbench-client.js"; @@ -93,6 +93,11 @@ class RemoteWorkbenchStore implements HandoffWorkbenchClient { await this.refresh(); } + async pushHandoff(input: HandoffWorkbenchSelectInput): Promise { + await this.backend.runAction(this.workspaceId, input.handoffId, "push"); + await this.refresh(); + } + async revertFile(input: HandoffWorkbenchDiffInput): Promise { await this.backend.revertWorkbenchFile(this.workspaceId, input); await this.refresh(); diff --git a/factory/packages/client/src/view-model.ts b/factory/packages/client/src/view-model.ts index 344f8a5..99ce33a 100644 --- a/factory/packages/client/src/view-model.ts +++ b/factory/packages/client/src/view-model.ts @@ -1,4 +1,4 @@ -import type { HandoffRecord, HandoffStatus } from "@openhandoff/shared"; +import type { HandoffRecord, HandoffStatus } from "@sandbox-agent/factory-shared"; export const HANDOFF_STATUS_GROUPS = [ "queued", diff --git a/factory/packages/client/src/workbench-client.ts b/factory/packages/client/src/workbench-client.ts index 1738c19..7f26dbf 100644 --- a/factory/packages/client/src/workbench-client.ts +++ b/factory/packages/client/src/workbench-client.ts @@ -12,9 +12,9 @@ import type { HandoffWorkbenchSnapshot, HandoffWorkbenchTabInput, HandoffWorkbenchUpdateDraftInput, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { BackendClient } from "./backend-client.js"; -import { getSharedMockWorkbenchClient } from "./mock/workbench-client.js"; +import { getMockWorkbenchClient } from "./mock/workbench-client.js"; import { createRemoteWorkbenchClient } from "./remote/workbench-client.js"; export type HandoffWorkbenchClientMode = "mock" | "remote"; @@ -34,6 +34,7 @@ export interface HandoffWorkbenchClient { renameBranch(input: HandoffWorkbenchRenameInput): Promise; archiveHandoff(input: HandoffWorkbenchSelectInput): Promise; publishPr(input: HandoffWorkbenchSelectInput): Promise; + pushHandoff(input: HandoffWorkbenchSelectInput): Promise; revertFile(input: HandoffWorkbenchDiffInput): Promise; updateDraft(input: HandoffWorkbenchUpdateDraftInput): Promise; sendMessage(input: HandoffWorkbenchSendMessageInput): Promise; @@ -49,7 +50,7 @@ export function createHandoffWorkbenchClient( options: CreateHandoffWorkbenchClientOptions, ): HandoffWorkbenchClient { if (options.mode === "mock") { - return getSharedMockWorkbenchClient(); + return getMockWorkbenchClient(options.workspaceId); } if (!options.backend) { diff --git a/factory/packages/client/src/workbench-model.ts b/factory/packages/client/src/workbench-model.ts index 51dd4f5..7e7a896 100644 --- a/factory/packages/client/src/workbench-model.ts +++ b/factory/packages/client/src/workbench-model.ts @@ -12,7 +12,7 @@ import type { WorkbenchProjectSection, WorkbenchRepo, WorkbenchTranscriptEvent as TranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; export const MODEL_GROUPS: ModelGroup[] = [ { @@ -913,7 +913,7 @@ export function buildInitialHandoffs(): Handoff[] { ]; } -export function buildInitialMockLayoutViewModel(): HandoffWorkbenchSnapshot { +export function buildInitialMockLayoutViewModel(workspaceId = "default"): HandoffWorkbenchSnapshot { const repos: WorkbenchRepo[] = [ { id: "acme-backend", label: "acme/backend" }, { id: "acme-frontend", label: "acme/frontend" }, @@ -921,7 +921,7 @@ export function buildInitialMockLayoutViewModel(): HandoffWorkbenchSnapshot { ]; const handoffs = buildInitialHandoffs(); return { - workspaceId: "default", + workspaceId, repos, projects: groupWorkbenchProjects(repos, handoffs), handoffs, @@ -960,6 +960,5 @@ export function groupWorkbenchProjects(repos: WorkbenchRepo[], handoffs: Handoff updatedAtMs: project.handoffs.length > 0 ? Math.max(...project.handoffs.map((handoff) => handoff.updatedAtMs)) : project.updatedAtMs, })) - .filter((project) => project.handoffs.length > 0) .sort((a, b) => b.updatedAtMs - a.updatedAtMs); } diff --git a/factory/packages/client/src/workbench.ts b/factory/packages/client/src/workbench.ts new file mode 100644 index 0000000..162025d --- /dev/null +++ b/factory/packages/client/src/workbench.ts @@ -0,0 +1 @@ +export * from "./workbench-client.js"; diff --git a/factory/packages/client/test/e2e/full-integration-e2e.test.ts b/factory/packages/client/test/e2e/full-integration-e2e.test.ts index 74f29d4..3f5e52a 100644 --- a/factory/packages/client/test/e2e/full-integration-e2e.test.ts +++ b/factory/packages/client/test/e2e/full-integration-e2e.test.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { describe, expect, it } from "vitest"; -import type { HistoryEvent, RepoOverview } from "@openhandoff/shared"; +import type { HistoryEvent, RepoOverview } from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_FULL_E2E = process.env.HF_ENABLE_DAEMON_FULL_E2E === "1"; diff --git a/factory/packages/client/test/e2e/github-pr-e2e.test.ts b/factory/packages/client/test/e2e/github-pr-e2e.test.ts index bd489fa..e3cbe8e 100644 --- a/factory/packages/client/test/e2e/github-pr-e2e.test.ts +++ b/factory/packages/client/test/e2e/github-pr-e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord, HistoryEvent } from "@openhandoff/shared"; +import type { HandoffRecord, HistoryEvent } from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_E2E = process.env.HF_ENABLE_DAEMON_E2E === "1"; diff --git a/factory/packages/client/test/e2e/workbench-e2e.test.ts b/factory/packages/client/test/e2e/workbench-e2e.test.ts index 00e8167..3807955 100644 --- a/factory/packages/client/test/e2e/workbench-e2e.test.ts +++ b/factory/packages/client/test/e2e/workbench-e2e.test.ts @@ -1,13 +1,15 @@ import { execFile } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import type { + HandoffRecord, HandoffWorkbenchSnapshot, WorkbenchAgentTab, WorkbenchHandoff, WorkbenchModelId, WorkbenchTranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_WORKBENCH_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_E2E === "1"; @@ -21,6 +23,10 @@ function requiredEnv(name: string): string { return value; } +function requiredRepoRemote(): string { + return process.env.HF_E2E_REPO_REMOTE?.trim() || requiredEnv("HF_E2E_GITHUB_REPO"); +} + function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId { const value = process.env[name]?.trim(); switch (value) { @@ -38,14 +44,66 @@ async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)); } -async function seedSandboxFile(workspaceId: string, handoffId: string, filePath: string, content: string): Promise { - const repoPath = `/root/.local/share/openhandoff/local-sandboxes/${workspaceId}/${handoffId}/repo`; +function backendPortFromEndpoint(endpoint: string): string { + const url = new URL(endpoint); + if (url.port) { + return url.port; + } + return url.protocol === "https:" ? "443" : "80"; +} + +async function resolveBackendContainerName(endpoint: string): Promise { + const explicit = process.env.HF_E2E_BACKEND_CONTAINER?.trim(); + if (explicit) { + if (explicit.toLowerCase() === "host") { + return null; + } + return explicit; + } + + const { stdout } = await execFileAsync("docker", [ + "ps", + "--filter", + `publish=${backendPortFromEndpoint(endpoint)}`, + "--format", + "{{.Names}}", + ]); + const containerName = stdout + .split("\n") + .map((line) => line.trim()) + .find(Boolean); + + return containerName ?? null; +} + +function sandboxRepoPath(record: HandoffRecord): string { + const activeSandbox = + record.sandboxes.find((sandbox) => sandbox.sandboxId === record.activeSandboxId) ?? + record.sandboxes.find((sandbox) => typeof sandbox.cwd === "string" && sandbox.cwd.length > 0); + const cwd = activeSandbox?.cwd?.trim(); + if (!cwd) { + throw new Error(`No sandbox cwd is available for handoff ${record.handoffId}`); + } + return cwd; +} + +async function seedSandboxFile(endpoint: string, record: HandoffRecord, filePath: string, content: string): Promise { + const repoPath = sandboxRepoPath(record); + const containerName = await resolveBackendContainerName(endpoint); + if (!containerName) { + const directory = + filePath.includes("/") ? `${repoPath}/${filePath.slice(0, filePath.lastIndexOf("/"))}` : repoPath; + await mkdir(directory, { recursive: true }); + await writeFile(`${repoPath}/${filePath}`, `${content}\n`, "utf8"); + return; + } + const script = [ `cd ${JSON.stringify(repoPath)}`, `mkdir -p ${JSON.stringify(filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".")}`, `printf '%s\\n' ${JSON.stringify(content)} > ${JSON.stringify(filePath)}`, ].join(" && "); - await execFileAsync("docker", ["exec", "openhandoff-backend-1", "bash", "-lc", script]); + await execFileAsync("docker", ["exec", containerName, "bash", "-lc", script]); } async function poll( @@ -166,7 +224,7 @@ describe("e2e(client): workbench flows", () => { const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet"; const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default"; - const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO"); + const repoRemote = requiredRepoRemote(); const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o"); const runId = `wb-${Date.now().toString(36)}`; const expectedFile = `${runId}.txt`; @@ -215,7 +273,8 @@ describe("e2e(client): workbench flows", () => { expect(findTab(initialCompleted, primaryTab.id).sessionId).toBeTruthy(); expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true); - await seedSandboxFile(workspaceId, created.handoffId, expectedFile, runId); + const detail = await client.getHandoff(workspaceId, created.handoffId); + await seedSandboxFile(endpoint, detail, expectedFile, runId); const fileSeeded = await poll( "seeded sandbox file reflected in workbench", diff --git a/factory/packages/client/test/e2e/workbench-load-e2e.test.ts b/factory/packages/client/test/e2e/workbench-load-e2e.test.ts index 230ae49..c1a01f6 100644 --- a/factory/packages/client/test/e2e/workbench-load-e2e.test.ts +++ b/factory/packages/client/test/e2e/workbench-load-e2e.test.ts @@ -5,7 +5,7 @@ import type { WorkbenchHandoff, WorkbenchModelId, WorkbenchTranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E === "1"; @@ -18,6 +18,10 @@ function requiredEnv(name: string): string { return value; } +function requiredRepoRemote(): string { + return process.env.HF_E2E_REPO_REMOTE?.trim() || requiredEnv("HF_E2E_GITHUB_REPO"); +} + function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId { const value = process.env[name]?.trim(); switch (value) { @@ -196,7 +200,7 @@ describe("e2e(client): workbench load", () => { async () => { const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet"; const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default"; - const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO"); + const repoRemote = requiredRepoRemote(); const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o"); const handoffCount = intEnv("HF_LOAD_HANDOFF_COUNT", 3); const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2); diff --git a/factory/packages/client/test/view-model.test.ts b/factory/packages/client/test/view-model.test.ts index 823ab7d..fac0ac0 100644 --- a/factory/packages/client/test/view-model.test.ts +++ b/factory/packages/client/test/view-model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord } from "@openhandoff/shared"; +import type { HandoffRecord } from "@sandbox-agent/factory-shared"; import { filterHandoffs, formatRelativeAge, diff --git a/factory/packages/client/test/workbench-client.test.ts b/factory/packages/client/test/workbench-client.test.ts new file mode 100644 index 0000000..79d7bd0 --- /dev/null +++ b/factory/packages/client/test/workbench-client.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; +import type { BackendClient } from "../src/backend-client.js"; +import { createHandoffWorkbenchClient } from "../src/workbench-client.js"; + +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("createHandoffWorkbenchClient", () => { + it("scopes mock clients by workspace", async () => { + const alpha = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-alpha", + }); + const beta = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-beta", + }); + + const alphaInitial = alpha.getSnapshot(); + const betaInitial = beta.getSnapshot(); + expect(alphaInitial.workspaceId).toBe("mock-alpha"); + expect(betaInitial.workspaceId).toBe("mock-beta"); + + await alpha.createHandoff({ + repoId: alphaInitial.repos[0]!.id, + task: "Ship alpha-only change", + title: "Alpha only", + }); + + expect(alpha.getSnapshot().handoffs).toHaveLength(alphaInitial.handoffs.length + 1); + expect(beta.getSnapshot().handoffs).toHaveLength(betaInitial.handoffs.length); + }); + + it("uses the initial task to bootstrap a new mock handoff session", async () => { + const client = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-onboarding", + }); + const snapshot = client.getSnapshot(); + + const created = await client.createHandoff({ + repoId: snapshot.repos[0]!.id, + task: "Reply with exactly: MOCK_WORKBENCH_READY", + title: "Mock onboarding", + branch: "feat/mock-onboarding", + model: "gpt-4o", + }); + + const runningHandoff = client.getSnapshot().handoffs.find((handoff) => handoff.id === created.handoffId); + expect(runningHandoff).toEqual( + expect.objectContaining({ + title: "Mock onboarding", + branch: "feat/mock-onboarding", + status: "running", + }), + ); + expect(runningHandoff?.tabs[0]).toEqual( + expect.objectContaining({ + id: created.tabId, + created: true, + status: "running", + }), + ); + expect(runningHandoff?.tabs[0]?.transcript).toEqual([ + expect.objectContaining({ + sender: "client", + payload: expect.objectContaining({ + method: "session/prompt", + }), + }), + ]); + + await sleep(2_700); + + const completedHandoff = client.getSnapshot().handoffs.find((handoff) => handoff.id === created.handoffId); + expect(completedHandoff?.status).toBe("idle"); + expect(completedHandoff?.tabs[0]).toEqual( + expect.objectContaining({ + status: "idle", + unread: true, + }), + ); + expect(completedHandoff?.tabs[0]?.transcript).toEqual([ + expect.objectContaining({ sender: "client" }), + expect.objectContaining({ sender: "agent" }), + ]); + }); + + it("routes remote push actions through the backend boundary", async () => { + const actions: Array<{ workspaceId: string; handoffId: string; action: string }> = []; + let snapshotReads = 0; + const backend = { + async runAction(workspaceId: string, handoffId: string, action: string): Promise { + actions.push({ workspaceId, handoffId, action }); + }, + async getWorkbench(workspaceId: string) { + snapshotReads += 1; + return { + workspaceId, + repos: [], + projects: [], + handoffs: [], + }; + }, + subscribeWorkbench(): () => void { + return () => {}; + }, + } as unknown as BackendClient; + + const client = createHandoffWorkbenchClient({ + mode: "remote", + backend, + workspaceId: "remote-ws", + }); + + await client.pushHandoff({ handoffId: "handoff-123" }); + + expect(actions).toEqual([ + { + workspaceId: "remote-ws", + handoffId: "handoff-123", + action: "push", + }, + ]); + expect(snapshotReads).toBe(1); + }); +}); diff --git a/factory/packages/frontend-errors/package.json b/factory/packages/frontend-errors/package.json index c30f796..77caa47 100644 --- a/factory/packages/frontend-errors/package.json +++ b/factory/packages/frontend-errors/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/frontend-errors", + "name": "@sandbox-agent/factory-frontend-errors", "version": "0.1.0", "private": true, "type": "module", diff --git a/factory/packages/frontend-errors/src/client.ts b/factory/packages/frontend-errors/src/client.ts index f055704..9981d80 100644 --- a/factory/packages/frontend-errors/src/client.ts +++ b/factory/packages/frontend-errors/src/client.ts @@ -6,8 +6,8 @@ interface FrontendErrorCollectorGlobal { declare global { interface Window { - __OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__?: FrontendErrorCollectorGlobal; - __OPENHANDOFF_FRONTEND_ERROR_CONTEXT__?: FrontendErrorContext; + __FACTORY_FRONTEND_ERROR_COLLECTOR__?: FrontendErrorCollectorGlobal; + __FACTORY_FRONTEND_ERROR_CONTEXT__?: FrontendErrorContext; } } @@ -17,11 +17,11 @@ export function setFrontendErrorContext(context: FrontendErrorContext): void { } const nextContext = sanitizeContext(context); - window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ = { - ...(window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ ?? {}), + window.__FACTORY_FRONTEND_ERROR_CONTEXT__ = { + ...(window.__FACTORY_FRONTEND_ERROR_CONTEXT__ ?? {}), ...nextContext, }; - window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__?.setContext(nextContext); + window.__FACTORY_FRONTEND_ERROR_COLLECTOR__?.setContext(nextContext); } function sanitizeContext(input: FrontendErrorContext): FrontendErrorContext { diff --git a/factory/packages/frontend-errors/src/router.ts b/factory/packages/frontend-errors/src/router.ts index aa0bbe7..f3b9d4d 100644 --- a/factory/packages/frontend-errors/src/router.ts +++ b/factory/packages/frontend-errors/src/router.ts @@ -4,8 +4,8 @@ import { dirname, join, resolve } from "node:path"; import { Hono } from "hono"; import type { FrontendErrorContext, FrontendErrorKind, FrontendErrorLogEvent } from "./types.js"; -const DEFAULT_RELATIVE_LOG_PATH = ".openhandoff/logs/frontend-errors.ndjson"; -const DEFAULT_REPORTER = "openhandoff-frontend"; +const DEFAULT_RELATIVE_LOG_PATH = ".sandbox-agent-factory/logs/frontend-errors.ndjson"; +const DEFAULT_REPORTER = "sandbox-agent-factory"; const MAX_FIELD_LENGTH = 12_000; export interface FrontendErrorCollectorRouterOptions { diff --git a/factory/packages/frontend-errors/src/script.ts b/factory/packages/frontend-errors/src/script.ts index 66101bf..025cc39 100644 --- a/factory/packages/frontend-errors/src/script.ts +++ b/factory/packages/frontend-errors/src/script.ts @@ -1,6 +1,6 @@ import type { FrontendErrorCollectorScriptOptions } from "./types.js"; -const DEFAULT_REPORTER = "openhandoff-frontend"; +const DEFAULT_REPORTER = "sandbox-agent-factory"; export function createFrontendErrorCollectorScript( options: FrontendErrorCollectorScriptOptions @@ -17,13 +17,13 @@ export function createFrontendErrorCollectorScript( return; } - if (window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__) { + if (window.__FACTORY_FRONTEND_ERROR_COLLECTOR__) { return; } var config = ${JSON.stringify(config)}; - var sharedContext = window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ || {}; - window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ = sharedContext; + var sharedContext = window.__FACTORY_FRONTEND_ERROR_CONTEXT__ || {}; + window.__FACTORY_FRONTEND_ERROR_CONTEXT__ = sharedContext; function now() { return Date.now(); @@ -124,7 +124,7 @@ export function createFrontendErrorCollectorScript( }); } - window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__ = { + window.__FACTORY_FRONTEND_ERROR_COLLECTOR__ = { setContext: function (nextContext) { if (!nextContext || typeof nextContext !== "object") { return; diff --git a/factory/packages/frontend-errors/src/vite.ts b/factory/packages/frontend-errors/src/vite.ts index f52eccb..312d65c 100644 --- a/factory/packages/frontend-errors/src/vite.ts +++ b/factory/packages/frontend-errors/src/vite.ts @@ -4,7 +4,7 @@ import type { Plugin } from "vite"; import { createFrontendErrorCollectorRouter, defaultFrontendErrorLogPath } from "./router.js"; import { createFrontendErrorCollectorScript } from "./script.js"; -const DEFAULT_MOUNT_PATH = "/__openhandoff/frontend-errors"; +const DEFAULT_MOUNT_PATH = "/__factory/frontend-errors"; const DEFAULT_EVENT_PATH = "/events"; export interface FrontendErrorCollectorVitePluginOptions { @@ -20,7 +20,7 @@ export function frontendErrorCollectorVitePlugin( ): Plugin { const mountPath = normalizePath(options.mountPath ?? DEFAULT_MOUNT_PATH); const logFilePath = options.logFilePath ?? defaultFrontendErrorLogPath(process.cwd()); - const reporter = options.reporter ?? "openhandoff-vite"; + const reporter = options.reporter ?? "factory-vite"; const endpoint = `${mountPath}${DEFAULT_EVENT_PATH}`; const router = createFrontendErrorCollectorRouter({ @@ -31,7 +31,7 @@ export function frontendErrorCollectorVitePlugin( const listener = getRequestListener(mountApp.fetch); return { - name: "openhandoff:frontend-error-collector", + name: "factory:frontend-error-collector", apply: "serve", transformIndexHtml(html) { return { diff --git a/factory/packages/frontend-errors/test/router.test.ts b/factory/packages/frontend-errors/test/router.test.ts index bed1d13..235a585 100644 --- a/factory/packages/frontend-errors/test/router.test.ts +++ b/factory/packages/frontend-errors/test/router.test.ts @@ -47,9 +47,9 @@ describe("frontend error collector router", () => { describe("frontend error collector script", () => { test("embeds configured endpoint", () => { const script = createFrontendErrorCollectorScript({ - endpoint: "/__openhandoff/frontend-errors/events", + endpoint: "/__factory/frontend-errors/events", }); - expect(script).toContain("/__openhandoff/frontend-errors/events"); + expect(script).toContain("/__factory/frontend-errors/events"); expect(script).toContain("window.addEventListener(\"error\""); }); }); diff --git a/factory/packages/frontend/index.html b/factory/packages/frontend/index.html index f4d55ce..6506468 100644 --- a/factory/packages/frontend/index.html +++ b/factory/packages/frontend/index.html @@ -10,7 +10,7 @@ - OpenHandoff + Sandbox Agent Factory
diff --git a/factory/packages/frontend/package.json b/factory/packages/frontend/package.json index 4d00b9f..09c1871 100644 --- a/factory/packages/frontend/package.json +++ b/factory/packages/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/frontend", + "name": "@sandbox-agent/factory-frontend", "version": "0.1.0", "private": true, "type": "module", @@ -10,9 +10,9 @@ "test": "vitest run" }, "dependencies": { - "@openhandoff/client": "workspace:*", - "@openhandoff/frontend-errors": "workspace:*", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-client": "workspace:*", + "@sandbox-agent/factory-frontend-errors": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "@tanstack/react-query": "^5.85.5", "@tanstack/react-router": "^1.132.23", "baseui": "^16.1.1", diff --git a/factory/packages/frontend/src/app/router.tsx b/factory/packages/frontend/src/app/router.tsx index 2719736..84faa8d 100644 --- a/factory/packages/frontend/src/app/router.tsx +++ b/factory/packages/frontend/src/app/router.tsx @@ -1,5 +1,5 @@ -import { useEffect } from "react"; -import { setFrontendErrorContext } from "@openhandoff/frontend-errors/client"; +import { useEffect, useSyncExternalStore } from "react"; +import { setFrontendErrorContext } from "@sandbox-agent/factory-frontend-errors/client"; import { Navigate, Outlet, @@ -10,7 +10,7 @@ import { } from "@tanstack/react-router"; import { MockLayout } from "../components/mock-layout"; import { defaultWorkspaceId } from "../lib/env"; -import { handoffWorkbenchClient } from "../lib/workbench"; +import { getHandoffWorkbenchClient, resolveRepoRouteHandoffId } from "../lib/workbench"; const rootRoute = createRootRoute({ component: RootLayout, @@ -74,18 +74,27 @@ function WorkspaceLayoutRoute() { function WorkspaceRoute() { const { workspaceId } = workspaceRoute.useParams(); + const client = getHandoffWorkbenchClient(workspaceId); useEffect(() => { setFrontendErrorContext({ workspaceId, handoffId: undefined, }); }, [workspaceId]); - return ; + return ( + + ); } function HandoffRoute() { const { workspaceId, handoffId } = handoffRoute.useParams(); const { sessionId } = handoffRoute.useSearch(); + const client = getHandoffWorkbenchClient(workspaceId); useEffect(() => { setFrontendErrorContext({ workspaceId, @@ -93,11 +102,24 @@ function HandoffRoute() { repoId: undefined, }); }, [handoffId, workspaceId]); - return ; + return ( + + ); } function RepoRoute() { const { workspaceId, repoId } = repoRoute.useParams(); + const client = getHandoffWorkbenchClient(workspaceId); + const snapshot = useSyncExternalStore( + client.subscribe.bind(client), + client.getSnapshot.bind(client), + client.getSnapshot.bind(client), + ); useEffect(() => { setFrontendErrorContext({ workspaceId, @@ -105,9 +127,7 @@ function RepoRoute() { repoId, }); }, [repoId, workspaceId]); - const activeHandoffId = handoffWorkbenchClient.getSnapshot().handoffs.find( - (handoff) => handoff.repoId === repoId, - )?.id; + const activeHandoffId = resolveRepoRouteHandoffId(snapshot, repoId); if (!activeHandoffId) { return ( { setEditingField(field); @@ -197,13 +199,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ } if (field === "title") { - void handoffWorkbenchClient.renameHandoff({ handoffId: handoff.id, value }); + void client.renameHandoff({ handoffId: handoff.id, value }); } else { - void handoffWorkbenchClient.renameBranch({ handoffId: handoff.id, value }); + void client.renameBranch({ handoffId: handoff.id, value }); } setEditingField(null); }, - [editValue, handoff.id], + [client, editValue, handoff.id], ); const updateDraft = useCallback( @@ -212,14 +214,14 @@ const TranscriptPanel = memo(function TranscriptPanel({ return; } - void handoffWorkbenchClient.updateDraft({ + void client.updateDraft({ handoffId: handoff.id, tabId: promptTab.id, text: nextText, attachments: nextAttachments, }); }, - [handoff.id, promptTab], + [client, handoff.id, promptTab], ); const sendMessage = useCallback(() => { @@ -230,24 +232,24 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSetActiveTabId(promptTab.id); onSetLastAgentTabId(promptTab.id); - void handoffWorkbenchClient.sendMessage({ + void client.sendMessage({ handoffId: handoff.id, tabId: promptTab.id, text, attachments, }); - }, [attachments, draft, handoff.id, onSetActiveTabId, onSetLastAgentTabId, promptTab]); + }, [attachments, client, draft, handoff.id, onSetActiveTabId, onSetLastAgentTabId, promptTab]); const stopAgent = useCallback(() => { if (!promptTab) { return; } - void handoffWorkbenchClient.stopAgent({ + void client.stopAgent({ handoffId: handoff.id, tabId: promptTab.id, }); - }, [handoff.id, promptTab]); + }, [client, handoff.id, promptTab]); const switchTab = useCallback( (tabId: string) => { @@ -257,7 +259,7 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSetLastAgentTabId(tabId); const tab = handoff.tabs.find((candidate) => candidate.id === tabId); if (tab?.unread) { - void handoffWorkbenchClient.setSessionUnread({ + void client.setSessionUnread({ handoffId: handoff.id, tabId, unread: false, @@ -266,14 +268,14 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSyncRouteSession(handoff.id, tabId); } }, - [handoff.id, handoff.tabs, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], + [client, handoff.id, handoff.tabs, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const setTabUnread = useCallback( (tabId: string, unread: boolean) => { - void handoffWorkbenchClient.setSessionUnread({ handoffId: handoff.id, tabId, unread }); + void client.setSessionUnread({ handoffId: handoff.id, tabId, unread }); }, - [handoff.id], + [client, handoff.id], ); const startRenamingTab = useCallback( @@ -305,13 +307,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ return; } - void handoffWorkbenchClient.renameSession({ + void client.renameSession({ handoffId: handoff.id, tabId: editingSessionTabId, title: trimmedName, }); cancelTabRename(); - }, [cancelTabRename, editingSessionName, editingSessionTabId, handoff.id]); + }, [cancelTabRename, client, editingSessionName, editingSessionTabId, handoff.id]); const closeTab = useCallback( (tabId: string) => { @@ -326,9 +328,9 @@ const TranscriptPanel = memo(function TranscriptPanel({ } onSyncRouteSession(handoff.id, nextTabId); - void handoffWorkbenchClient.closeTab({ handoffId: handoff.id, tabId }); + void client.closeTab({ handoffId: handoff.id, tabId }); }, - [activeTabId, handoff.id, handoff.tabs, lastAgentTabId, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], + [activeTabId, client, handoff.id, handoff.tabs, lastAgentTabId, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const closeDiffTab = useCallback( @@ -346,12 +348,12 @@ const TranscriptPanel = memo(function TranscriptPanel({ const addTab = useCallback(() => { void (async () => { - const { tabId } = await handoffWorkbenchClient.addTab({ handoffId: handoff.id }); + const { tabId } = await client.addTab({ handoffId: handoff.id }); onSetLastAgentTabId(tabId); onSetActiveTabId(tabId); onSyncRouteSession(handoff.id, tabId); })(); - }, [handoff.id, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession]); + }, [client, handoff.id, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession]); const changeModel = useCallback( (model: ModelId) => { @@ -359,13 +361,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ throw new Error(`Unable to change model for handoff ${handoff.id} without an active prompt tab`); } - void handoffWorkbenchClient.changeModel({ + void client.changeModel({ handoffId: handoff.id, tabId: promptTab.id, model, }); }, - [handoff.id, promptTab], + [client, handoff.id, promptTab], ); const addAttachment = useCallback( @@ -551,17 +553,18 @@ const TranscriptPanel = memo(function TranscriptPanel({ }); interface MockLayoutProps { + client: HandoffWorkbenchClient; workspaceId: string; selectedHandoffId?: string | null; selectedSessionId?: string | null; } -export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }: MockLayoutProps) { +export function MockLayout({ client, workspaceId, selectedHandoffId, selectedSessionId }: MockLayoutProps) { const navigate = useNavigate(); const viewModel = useSyncExternalStore( - handoffWorkbenchClient.subscribe.bind(handoffWorkbenchClient), - handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), - handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), + client.subscribe.bind(client), + client.getSnapshot.bind(client), + client.getSnapshot.bind(client), ); const handoffs = viewModel.handoffs ?? []; const projects = viewModel.projects ?? []; @@ -668,7 +671,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } const title = window.prompt("Optional handoff title", "")?.trim() || undefined; const branch = window.prompt("Optional branch name", "")?.trim() || undefined; - const { handoffId, tabId } = await handoffWorkbenchClient.createHandoff({ + const { handoffId, tabId } = await client.createHandoff({ repoId, task, model: "gpt-4o", @@ -684,7 +687,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } search: { sessionId: tabId ?? undefined }, }); })(); - }, [activeHandoff?.repoId, navigate, viewModel.repos, workspaceId]); + }, [activeHandoff?.repoId, client, navigate, viewModel.repos, workspaceId]); const openDiffTab = useCallback( (path: string) => { @@ -726,8 +729,8 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } ); const markHandoffUnread = useCallback((id: string) => { - void handoffWorkbenchClient.markHandoffUnread({ handoffId: id }); - }, []); + void client.markHandoffUnread({ handoffId: id }); + }, [client]); const renameHandoff = useCallback( (id: string) => { @@ -746,9 +749,9 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } return; } - void handoffWorkbenchClient.renameHandoff({ handoffId: id, value: trimmedTitle }); + void client.renameHandoff({ handoffId: id, value: trimmedTitle }); }, - [handoffs], + [client, handoffs], ); const renameBranch = useCallback( @@ -768,24 +771,31 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } return; } - void handoffWorkbenchClient.renameBranch({ handoffId: id, value: trimmedBranch }); + void client.renameBranch({ handoffId: id, value: trimmedBranch }); }, - [handoffs], + [client, handoffs], ); const archiveHandoff = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot archive without an active handoff"); } - void handoffWorkbenchClient.archiveHandoff({ handoffId: activeHandoff.id }); - }, [activeHandoff]); + void client.archiveHandoff({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); const publishPr = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot publish PR without an active handoff"); } - void handoffWorkbenchClient.publishPr({ handoffId: activeHandoff.id }); - }, [activeHandoff]); + void client.publishPr({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); + + const pushHandoff = useCallback(() => { + if (!activeHandoff) { + throw new Error("Cannot push without an active handoff"); + } + void client.pushHandoff({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); const revertFile = useCallback( (path: string) => { @@ -804,18 +814,20 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } : current[activeHandoff.id] ?? null, })); - void handoffWorkbenchClient.revertFile({ + void client.revertFile({ handoffId: activeHandoff.id, path, }); }, - [activeHandoff, lastAgentTabIdByHandoff], + [activeHandoff, client, lastAgentTabIdByHandoff], ); if (!activeHandoff) { return ( diff --git a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx index ae8a1c0..d8927f1 100644 --- a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx @@ -15,6 +15,49 @@ import { import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui"; import { type FileTreeNode, type Handoff, diffTabId } from "./view-model"; +const StatusCard = memo(function StatusCard({ + label, + value, + mono = false, +}: { + label: string; + value: string; + mono?: boolean; +}) { + const [css, theme] = useStyletron(); + + return ( +
+ + {label} + +
+ {value} +
+
+ ); +}); + const FileTree = memo(function FileTree({ nodes, depth, @@ -106,6 +149,7 @@ export const RightSidebar = memo(function RightSidebar({ activeTabId, onOpenDiff, onArchive, + onPush, onRevertFile, onPublishPr, }: { @@ -113,6 +157,7 @@ export const RightSidebar = memo(function RightSidebar({ activeTabId: string | null; onOpenDiff: (path: string) => void; onArchive: () => void; + onPush: () => void; onRevertFile: (path: string) => void; onPublishPr: () => void; }) { @@ -121,7 +166,12 @@ export const RightSidebar = memo(function RightSidebar({ const contextMenu = useContextMenu(); const changedPaths = useMemo(() => new Set(handoff.fileChanges.map((file) => file.path)), [handoff.fileChanges]); const isTerminal = handoff.status === "archived"; + const canPush = !isTerminal && Boolean(handoff.branch); const pullRequestUrl = handoff.pullRequest != null ? `https://github.com/${handoff.repoName}/pull/${handoff.pullRequest.number}` : null; + const pullRequestStatus = + handoff.pullRequest == null + ? "Not published" + : `#${handoff.pullRequest.number} ${handoff.pullRequest.status === "draft" ? "Draft" : "Ready"}`; const copyFilePath = useCallback(async (path: string) => { try { @@ -183,6 +233,7 @@ export const RightSidebar = memo(function RightSidebar({ {pullRequestUrl ? "Open PR" : "Publish PR"}