@bud.sh/app 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +41 -31
  2. package/dist/client/assets/bud/design.js +1 -1
  3. package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
  4. package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
  5. package/dist/client/assets/dist-CdE5wBkg.js +5 -0
  6. package/dist/client/assets/files-1dLuoM2f.js +1 -0
  7. package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
  8. package/dist/client/assets/index-Cn2mxroU.css +2 -0
  9. package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
  10. package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
  11. package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
  12. package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
  13. package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
  14. package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
  15. package/dist/client/assets/project-work-DAE9-olg.js +1 -0
  16. package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
  17. package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
  18. package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
  19. package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
  20. package/dist/client/index.html +3 -3
  21. package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
  22. package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
  23. package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
  24. package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
  25. package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
  26. package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
  27. package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
  28. package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
  29. package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
  30. package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
  31. package/dist/hosted/hosted.js +7885 -6219
  32. package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
  33. package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
  34. package/dist/node/assets/config-9_jWwNBh.js +1067 -0
  35. package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
  36. package/dist/node/assets/module-3GgtT4Xs.js +308 -0
  37. package/dist/node/assets/server-DTQFuoX2.js +846 -0
  38. package/dist/node/assets/src-D-kG1GxB.js +2 -0
  39. package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
  40. package/dist/node/assets/src-DysjfbM8.js +8592 -0
  41. package/dist/node/bridge.js +1 -845
  42. package/dist/node/cli.js +206 -0
  43. package/dist/node/design.js +811 -13
  44. package/dist/node/index.js +2 -739
  45. package/dist/node/plugins.js +5711 -0
  46. package/dist/node/server.js +1 -6746
  47. package/dist/node/vite.js +121 -0
  48. package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
  49. package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
  50. package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
  51. package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
  52. package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
  53. package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
  54. package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
  55. package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
  56. package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
  57. package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
  58. package/dist/node-host/node-host.js +6127 -4438
  59. package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
  60. package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
  61. package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
  62. package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
  63. package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
  64. package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
  65. package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
  66. package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
  67. package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
  68. package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
  69. package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
  70. package/dist/server/assets/run-BJ8r5jSh.js +572 -0
  71. package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
  72. package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
  73. package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
  74. package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
  75. package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
  76. package/dist/server/capture.js +1 -2
  77. package/dist/server/migration.js +569 -12
  78. package/dist/server/runtime.js +7 -5
  79. package/dist/server/server.js +3 -3
  80. package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
  81. package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
  82. package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
  83. package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
  84. package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
  85. package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
  86. package/dist/types/bridge.d.ts +1 -1
  87. package/dist/types/companion.d.ts +3 -3
  88. package/dist/types/design.d.ts +2 -2
  89. package/dist/types/hosted.d.ts +3 -3
  90. package/dist/types/index.d.ts +2 -2
  91. package/dist/types/node-host.d.ts +2 -2
  92. package/dist/types/plugins.d.ts +445 -0
  93. package/dist/types/screen.d.ts +2 -2
  94. package/dist/types/server.d.ts +2 -228
  95. package/dist/types/vite.d.ts +16 -0
  96. package/package.json +19 -3
  97. package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
  98. package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
  99. package/dist/client/assets/files-js6ElfkU.js +0 -1
  100. package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
  101. package/dist/client/assets/index-CFFxP0gS.css +0 -2
  102. package/dist/client/assets/pages-6_lhiQmz.js +0 -1
  103. package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
  104. package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
  105. package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
  106. package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
  107. package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
  108. package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
  109. package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
  110. package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
  111. package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
  112. package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
  113. package/dist/server/assets/tasks-CiXodsLN.js +0 -652
  114. package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
  115. package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
@@ -1,11 +1,11 @@
1
- import { $ as publishAll, $n as forkUnsafe, $t as provideContext, An as uninterruptible, At as catch_, Bn as effectDiscard, Bt as forever, Cn as tap, Ct as as$1, D as as, Dn as tryPromise, E as FileSystem, Fn as currentTimeMillis, Ft as fail$1, G as make$3, Gn as mergeAll, Gt as map, H as runForEach, Hn as isLayer, Ht as forkScoped, I as fromPubSub, In as build, It as filterOrFail, J as offerUnsafe, Jn as succeed$1, Jt as option, Kn as provide$1, Kt as mapError, L as ignore$1, Ln as buildWithMemoMap, Lt as flatMap, Mn as void_$1, N as empty$1, Nn as withFiber, Nt as die, O as callback, On as try_, Ot as catchIf, P as filter, Pn as yieldNow, Pt as ensuring, Q as publish, Qn as close, R as make$2, Rn as effect, Rt as flatten, S as writeTextAtomic, Sn as sync, St as andThen, T as Path, Tn as timeoutOption, Tt as asVoid, U as tap$1, Un as makeMemoMapUnsafe, Ut as gen, V as merge, Vn as empty, Vt as forkIn, W as unwrap$1, Wt as ignore, X as sliding$1, Xn as sync$1, Xt as orElseSucceed, Y as shutdown$1, Yt as orDie, Z as take, Zn as unwrap, Zt as promise, _n as serviceOption, _t as bytes$1, an as runFork, ar as void_, at as runIn, bn as succeedNone, bt as addFinalizer, cn as runPromiseExit, cr as add, dn as runSync, dr as getOrNull, en as provideService, er as makeUnsafe, et as shutdown, fn as runSyncExit, fr as getOrUndefined, gn as scoped, gr as TaggedError, hn as scope, hr as some, in as runCallbackWith, ir as succeed$2, it as interrupt, j as debounce, jn as uninterruptibleMask, jt as clockWith, kn as undefined_$1, kt as catchTag, ln as runPromiseExitWith, lr as getOption, m as projectModuleFile, mn as runSyncWith, mr as none, nr as doneUnsafe, nt as unbounded, on as runForkWith, or as scopeRemoveFinalizerUnsafe, pn as runSyncExitWith, pr as isNone, q as offer, rn as runCallback, rr as make$4, rt as await_, sn as runPromise, sr as Service, tn as result, tr as _await, tt as sliding, u as Git, un as runPromiseWith, ur as make$1, vn as sleep, vt as acquireRelease, w as RuntimePolicy, wn as tapError, wt as asSome, xn as suspend, xt as all, yn as succeed, zn as effectContext, zt as forEach } from "./branch-previews-ClkoJ0lM.js";
2
- import { $t as list, At as parseReviewSourceQuery, Cn as parseWorkValue, Ct as parseReviewDiffQuery, D as MAX_THREADS, Dn as parseMessage, En as messageDeletionRefusals, Fn as identifier, Ft as parseFileSearchQuery, G as applyReviewSelection, I as parseThreadInput, In as nonempty, Jt as isWithinDirectory, Kt as parseFileIndexQuery, L as parseThreadLinkInput, Ln as parseWork, Lt as classifyText, M as parseThreadCapabilities, Mn as parseWorkSchema, Mt as matchFileText, N as parseThreadDeleteInput, Nn as statusCategory, On as parseMessageAuthor, P as parseThreadFilter, Pn as WorkValidationError, Pt as searchPathMatches, Qt as integer, R as parseThreadReplyInput, Rn as record$1, Rt as filterFileEntries, Sn as parseWorkUpdate, Tn as messageDeletionRefusal, Tt as parseReviewHistoryQuery, V as parseThreadStatusInput, Vn as iconNames, Xt as parseFilePath, Yt as parentPath, Zt as pathHierarchy, _t as parseRecovery, an as parseWorkWorkspace, cn as parseSectionData, dn as parseWorkConnection, en as parseContract, fn as parseWorkCreate, ft as parseMutationApply, gn as parseWorkMutationResult, hn as parseWorkItemRef, i as routes, it as parseReviewActionIntent, j as parseThread, jn as fieldByRole, jt as compileSearchPattern, kn as withoutMessages, ln as parseWorkActionInput, lt as parseFileSaveRequest, mn as parseWorkItem, nn as strict$1, pn as parseWorkDetail, pt as parseMutationIntent, qt as isProtectedPath, r as parseLiveId, rt as parseReviewActionApply, st as parseReviewActionRecovery, t as RESYNC, tn as revision, vn as parseWorkQuery, vt as pathCollisionKey, wn as valueByRole, xt as parseReviewComparison, yn as parseWorkSnapshot, yt as validateMutationSelection, z as parseThreadSnapshot, zn as strict, zt as orderFileEntries } from "./events-iHpLGr3k.js";
3
- import { pathToFileURL } from "node:url";
4
- import { createHash, randomBytes, randomUUID } from "node:crypto";
1
+ import { $ as unwrap$1, $n as provide$1, $t as map, An as tap, At as andThen, B as ensuring$1, Bn as withFiber, Cn as scoped, Cr as TaggedError, Dn as succeedNone, En as succeed, Et as acquireRelease, Fn as try_, G as ignore$1, Gn as effect, Gt as flatMap, H as filter, Hn as currentTimeMillis, Ht as ensuring, In as undefined_$1, It as catchIf, Jn as empty, Jt as forever, K as make$1, Kt as flatten, L as debounce, Ln as uninterruptible, Lt as catchTag, M as Path, Mn as timeoutOption, Mt as asSome, N as FileSystem, Nt as asVoid, On as suspend, Ot as addFinalizer, P as as, Pn as tryPromise, Q as tap$1, Qn as mergeAll, Qt as ignore, Rn as uninterruptibleMask, Rt as catch_, Sn as scope, Sr as some, Tn as sleep, Tt as bytes$1, U as fromAsyncIterable, Un as build, Ut as fail$1, V as fail$2, Vn as yieldNow, Vt as die, W as fromPubSub, Wn as buildWithMemoMap, Wt as filterOrFail, X as runForEach, Xn as makeMemoMapUnsafe, Xt as forkScoped, Y as merge, Yn as isLayer, Yt as forkIn, Z as suspend$1, Zt as gen, _n as runPromiseWith, an as promise, ar as close, at as take, bn as runSyncExitWith, br as isNone, cn as provideService, cr as _await, ct as shutdown, dn as runCallbackWith, dr as succeed$2, dt as await_, en as mapError, et as make$2, f as PICKER_MODULE_URL, fn as runFork, fr as void_, ft as interrupt, gn as runPromiseExitWith, gr as getOption, hn as runPromiseExit, in as orElseSucceed, ir as unwrap, it as sliding$1, j as RuntimePolicy, jn as tapError, jt as as$1, k as writeTextAtomic, kn as sync, kt as all, ln as result, lr as doneUnsafe, lt as sliding, m as Git, mn as runPromise, mr as Service, nn as option, nt as offer, or as forkUnsafe, ot as publish, p as pickerModule, pn as runForkWith, pr as scopeRemoveFinalizerUnsafe, pt as runIn, qn as effectDiscard, qt as forEach, rn as orDie, rr as sync$1, rt as shutdown$1, sn as provideContext, sr as makeUnsafe, st as publishAll, t as BranchPreviews, tr as succeed$1, tt as makeUnsafe$1, u as bridgeScript, un as runCallback, ur as make$3, ut as unbounded, v as projectModuleFile, vn as runSync, vr as getOrNull, wn as serviceOption, xn as runSyncWith, xr as none, y as configPlugins, yn as runSyncExit, yr as getOrUndefined, z as empty$1, zn as void_$1, zt as clockWith } from "./branch-previews-DedrpnVR.js";
2
+ import { $t as parseContract, An as fieldByRole, At as compileSearchPattern, Bn as iconNames, En as parseMessage, Fn as nonempty, Gt as parseFileIndexQuery, In as parseWork, It as classifyText, Jt as parentPath, Kt as isProtectedPath, Ln as record$2, Lt as filterFileEntries, Nn as WorkValidationError, Nt as searchPathMatches, Pn as identifier, Pt as parseFileSearchQuery, Qt as list, R as parseThreadSnapshot, Rn as strict, Rt as orderFileEntries, Sn as valueByRole, St as parseReviewDiffQuery, W as applyReviewSelection, Xt as pathHierarchy, Yt as parseFilePath, Zt as integer, _n as parseWorkQuery, _t as pathCollisionKey, bt as parseReviewComparison, ct as parseFileSaveRequest, dt as parseMutationApply, en as revision, fn as parseWorkDetail, ft as parseMutationIntent, gt as parseRecovery, i as routes, in as parseWorkWorkspace, j as parseThread, jn as parseWorkSchema, jt as matchFileText, kt as parseReviewSourceQuery, nt as parseReviewActionApply, ot as parseReviewActionRecovery, qt as isWithinDirectory, r as parseLiveId, rt as parseReviewActionIntent, t as RESYNC, tn as strict$1, un as parseWorkConnection, vt as validateMutationSelection, wt as parseReviewHistoryQuery } from "./events-BH_rAu7O.js";
3
+ import { a as parseRunRecord, c as parseTaskContext, i as advanceRun, n as RUN_WAITING_STATES, o as runActive, r as RunTransitionError, s as startRun, t as NEEDS_CONTEXT_MARKER } from "./run-BJ8r5jSh.js";
4
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeSync } from "node:fs";
6
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { any, boolean, enum_ } from "remix/data-schema";
8
- import { open, realpath, stat } from "node:fs/promises";
8
+ import { mkdir, open, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
9
9
  //#region ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/ManagedRuntime.js
10
10
  var TypeId = "~effect/ManagedRuntime";
11
11
  /**
@@ -323,7 +323,7 @@ var ThreadImportSchema = strict({
323
323
  thread: any()
324
324
  });
325
325
  function history(value) {
326
- let input = parseWork(record$1, value, "import.thread");
326
+ let input = parseWork(record$2, value, "import.thread");
327
327
  if (!Array.isArray(input.messages) || input.messages.length === 0 || input.messages.length > 67108864 || input.messagesLoaded !== true || input.messagesTruncated !== false || input.messageCount !== input.messages.length) throw new WorkValidationError("import.thread.messages");
328
328
  let encoder = new TextEncoder();
329
329
  let bytes = 0;
@@ -374,755 +374,12 @@ function parseThreadImport(value) {
374
374
  thread: history(input.thread)
375
375
  };
376
376
  }
377
- //#endregion
378
- //#region ../work/src/server/memory-state.ts
379
- var expectedRevision = nonempty(256);
380
- var projectRoot = nonempty(4096);
381
- /**
382
- * Runs a parser over caller input, turning its failure into an `input` work error.
383
- * @param parse - Parses the input.
384
- * @returns The parsed input.
385
- */
386
- function input$3(parse) {
387
- try {
388
- return parse();
389
- } catch (cause) {
390
- if (cause instanceof WorkError) throw cause;
391
- throw new WorkError({
392
- reason: "input",
393
- message: "Check the supplied values.",
394
- cause
395
- });
396
- }
397
- }
398
- /**
399
- * Runs a parser over stored data, turning its failure into a `storage` work error.
400
- * @param parse - Parses the stored data.
401
- * @returns The parsed data.
402
- */
403
- function output(parse) {
404
- try {
405
- return parse();
406
- } catch (cause) {
407
- if (cause instanceof WorkError) throw cause;
408
- throw new WorkError({
409
- reason: "storage",
410
- message: "The adapter returned invalid data.",
411
- cause
412
- });
413
- }
414
- }
415
- /**
416
- * Throws a `refused` work error.
417
- * @param message - The reason, safe to show.
418
- */
419
- function refused(message) {
420
- throw new WorkError({
421
- reason: "refused",
422
- message
423
- });
424
- }
425
- /**
426
- * Throws a `conflict` work error unless a record is still at the revision the caller saw.
427
- * @param current - The stored record.
428
- * @param revision - The caller's expected revision.
429
- */
430
- function expected(current, revision) {
431
- input$3(() => parseWork(expectedRevision, revision, "expectedRevision"));
432
- if (current.revision !== revision) throw new WorkError({
433
- reason: "conflict",
434
- message: "This record changed. Refresh and review your changes before saving.",
435
- currentRevision: current.revision
436
- });
437
- }
438
- /**
439
- * A loaded section, cut to its limit.
440
- * @param items - The section's entries.
441
- * @param max - The most entries to return.
442
- * @param count - How many entries exist in all.
443
- * @returns The available section, truncated when more exist than it holds.
444
- */
445
- function available(items, max, count = items.length) {
446
- return {
447
- state: "available",
448
- items: items.slice(0, max),
449
- truncated: count > max
450
- };
451
- }
452
- /**
453
- * A section the store does not provide.
454
- * @returns The unsupported section.
455
- */
456
- function unsupported$1() {
457
- return {
458
- state: "unsupported",
459
- items: [],
460
- truncated: false
461
- };
462
- }
463
- /** The items, discussions and watchers behind the in-memory work and thread adapters. */
464
- var MemoryStore = class {
465
- identity = randomUUID();
466
- schema;
467
- author;
468
- defaults;
469
- capabilities;
470
- options;
471
- workspaces = /* @__PURE__ */ new Map();
472
- #watchers = /* @__PURE__ */ new Map();
473
- seed;
474
- constructor(options) {
475
- this.schema = parseWorkSchema(options.schema);
476
- this.author = parseMessageAuthor(options.author ?? {
477
- id: "memory",
478
- name: "Memory adapter",
479
- email: ""
480
- });
481
- this.defaults = Object.fromEntries(Object.entries(options.defaults ?? {}).map(([key, value]) => {
482
- let field = this.schema.fields.find((field) => field.id === key);
483
- if (!field) refused("A default refers to an undeclared field.");
484
- return [key, parseWorkValue(value, field, this.schema)];
485
- }));
486
- this.options = {
487
- ...options,
488
- actions: { ...options.actions },
489
- sections: { ...options.sections }
490
- };
491
- this.capabilities = parseThreadCapabilities({
492
- create: this.schema.capabilities.threads,
493
- reply: this.schema.capabilities.threads,
494
- status: this.schema.capabilities.threads,
495
- link: this.schema.capabilities.threads,
496
- delete: this.schema.capabilities.threads,
497
- ...options.threadCapabilities,
498
- sync: false,
499
- edit: false
500
- });
501
- if (this.schema.capabilities.sync) refused("Memory storage has no remote synchronization.");
502
- for (let action of this.schema.actions) if (action.kind === "custom" && !this.options.actions?.[action.id]) refused("Declare an implementation for each custom memory action.");
503
- for (let id of Object.keys(this.options.actions ?? {})) if (!this.schema.actions.some((action) => action.id === id)) refused("An action implementation has no declaration.");
504
- for (let id of Object.keys(this.options.sections ?? {})) if (!this.schema.sections.some((section) => section.id === id)) refused("A section implementation has no declaration.");
505
- this.seed = {
506
- sequence: 0,
507
- items: /* @__PURE__ */ new Map(),
508
- threads: /* @__PURE__ */ new Map()
509
- };
510
- for (let value of options.items ?? []) {
511
- let item = parseWorkItem(value, this.schema);
512
- if (!item.contentLoaded || !item.revision || this.seed.items.has(item.id)) refused("Seed work items need complete content and unique identities with revisions.");
513
- this.seed.items.set(item.id, {
514
- item,
515
- activity: [],
516
- activityCount: 0,
517
- history: [],
518
- historyCount: 0
519
- });
520
- }
521
- for (let value of options.threads ?? []) {
522
- let thread = parseThread(value);
523
- if (!thread.messagesLoaded || thread.messagesTruncated || !thread.revision || this.seed.threads.has(thread.id)) refused("Seed threads need complete messages and unique identities with revisions.");
524
- for (let id of thread.itemIds) this.item(this.seed, id);
525
- this.seed.threads.set(thread.id, thread);
526
- }
527
- for (let row of this.seed.items.values()) for (let relation of row.item.relations) this.item(this.seed, relation.item.id);
528
- }
529
- /**
530
- * Signals every write to one workspace while the stream runs.
531
- * @param root - The project directory.
532
- * @returns One element per write.
533
- */
534
- watch(root) {
535
- return callback((queue) => acquireRelease(try_({
536
- try: () => {
537
- let key = input$3(() => resolve(root));
538
- let notify = () => void offerUnsafe(queue, void 0);
539
- let watchers = this.#watchers.get(key) ?? /* @__PURE__ */ new Set();
540
- this.#watchers.set(key, watchers.add(notify));
541
- return () => watchers.delete(notify);
542
- },
543
- catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
544
- reason: "input",
545
- message: "Invalid project directory.",
546
- cause
547
- })
548
- }), (stop) => sync(stop)));
549
- }
550
- run(root, write, operation) {
551
- return gen({ self: this }, function* () {
552
- yield* yieldNow;
553
- let now = yield* currentTimeMillis;
554
- return yield* try_({
555
- try: () => {
556
- let key = input$3(() => {
557
- parseWork(projectRoot, root, "root");
558
- if (!isAbsolute(root)) throw new TypeError("The project directory must be absolute.");
559
- return resolve(root);
560
- });
561
- let current = this.workspaces.get(key) ?? structuredClone(this.seed);
562
- let data = write ? structuredClone(current) : current;
563
- let result = structuredClone(operation(data, new Date(now).toISOString()));
564
- if (write) {
565
- this.workspaces.set(key, data);
566
- for (let notify of [...this.#watchers.get(key) ?? []]) notify();
567
- }
568
- return result;
569
- },
570
- catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
571
- reason: "storage",
572
- message: "Memory storage failed.",
573
- cause
574
- })
575
- });
576
- });
577
- }
578
- next(data, prefix) {
579
- return `${prefix}-${this.identity}-${++data.sequence}`;
580
- }
581
- item(data, id) {
582
- input$3(() => parseWork(identifier, id, "id"));
583
- let item = data.items.get(id);
584
- if (!item) throw new WorkError({
585
- reason: "not-found",
586
- message: "This work item does not exist."
587
- });
588
- return item;
589
- }
590
- thread(data, id) {
591
- input$3(() => parseWork(identifier, id, "id"));
592
- let thread = data.threads.get(id);
593
- if (!thread) throw new WorkError({
594
- reason: "not-found",
595
- message: "This discussion does not exist."
596
- });
597
- return thread;
598
- }
599
- done(item) {
600
- let status = valueByRole(item, this.schema, "status");
601
- return typeof status === "string" && ["done", "cancelled"].includes(statusCategory(this.schema, status) ?? "");
602
- }
603
- view(data, id) {
604
- let item = this.item(data, id).item;
605
- let children = [...data.items.values()].filter((row) => row.item.relations.some((relation) => relation.direction === "out" && relation.item.id === id && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "parent"));
606
- let discussion = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).reduce((count, thread) => count + thread.messageCount, 0);
607
- let blocked = item.relations.some((relation) => relation.direction === "out" && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "blocks" && !this.done(this.item(data, relation.item.id).item));
608
- return output(() => parseWorkItem({
609
- ...item,
610
- counts: {
611
- ...this.schema.capabilities.discussion ? { discussion } : {},
612
- children: children.length,
613
- childrenDone: children.filter((row) => this.done(row.item)).length
614
- },
615
- ...this.schema.capabilities.ready ? { ready: !this.done(item) && !blocked } : {}
616
- }, this.schema));
617
- }
618
- save(data, value, at, summary) {
619
- let prior = data.items.get(value.id);
620
- let revision = this.next(data, "revision");
621
- let values = { ...value.values };
622
- for (let [role, entry] of [
623
- ["identifier", value.id],
624
- ["revision", revision],
625
- ["updated", at],
626
- ...!prior ? [["created", at]] : []
627
- ]) {
628
- let field = this.schema.fields.find((field) => field.role === role);
629
- if (field) values[field.id] = entry;
630
- }
631
- let item = output(() => parseWorkItem({
632
- ...value,
633
- revision,
634
- values
635
- }, this.schema));
636
- let row = prior ?? {
637
- item,
638
- history: [],
639
- historyCount: 0,
640
- activity: [],
641
- activityCount: 0
642
- };
643
- row.item = item;
644
- data.items.set(item.id, row);
645
- item = this.view(data, item.id);
646
- row.item = item;
647
- row.history.unshift({
648
- revision,
649
- author: this.author.name,
650
- at,
651
- item: structuredClone(item)
652
- });
653
- row.history = row.history.slice(0, 50);
654
- row.historyCount++;
655
- row.activity.unshift({
656
- id: this.next(data, "activity"),
657
- actor: this.author.name,
658
- at,
659
- verb: prior ? "changed" : "created",
660
- summary
661
- });
662
- row.activity = row.activity.slice(0, 200);
663
- row.activityCount++;
664
- return item;
665
- }
666
- touchThreads(data, ids, at) {
667
- for (let id of new Set(ids)) this.save(data, this.item(data, id).item, at, "Discussion changed");
668
- }
669
- initialValues(id, at, supplied) {
670
- let values = {
671
- ...this.defaults,
672
- ...supplied
673
- };
674
- let status = fieldByRole(this.schema, "status");
675
- if (status && values[status.id] === void 0 && this.schema.statuses.length) values[status.id] = this.schema.statuses[0].value;
676
- for (let role of [
677
- "identifier",
678
- "created",
679
- "updated"
680
- ]) {
681
- let field = fieldByRole(this.schema, role);
682
- if (field) values[field.id] = role === "identifier" ? id : at;
683
- }
684
- return values;
685
- }
686
- };
687
- //#endregion
688
- //#region ../work/src/server/memory-threads.ts
689
- /**
690
- * The thread adapter over an in-memory store.
691
- * @param store - The store shared with the work adapter.
692
- * @returns The `ThreadAdapter` implementation.
693
- */
694
- function memoryThreads(store) {
695
- function view(thread, brief = false) {
696
- let limit = brief ? 1 : 200;
697
- return output(() => parseThread({
698
- ...thread,
699
- messages: thread.messages.slice(0, limit),
700
- messagesLoaded: !brief,
701
- messagesTruncated: !brief && thread.messages.length > limit
702
- }));
703
- }
704
- function save(data, thread, at, oldIds = []) {
705
- let changed = {
706
- ...thread,
707
- revision: store.next(data, "thread-revision"),
708
- updatedAt: new Date(Math.max(Date.parse(at), Date.parse(thread.updatedAt))).toISOString(),
709
- messageCount: thread.messages.length
710
- };
711
- let result = view(changed);
712
- data.threads.set(changed.id, changed);
713
- store.touchThreads(data, [...oldIds, ...changed.itemIds], at);
714
- return result;
715
- }
716
- function snapshot(data, filter) {
717
- let threads = [...data.threads.values()].filter((thread) => {
718
- if (filter.itemId && !thread.itemIds.includes(filter.itemId)) return false;
719
- let kind = thread.anchor ? "type" in thread.anchor ? "code" : "canvas" : null;
720
- if (filter.anchorKind && filter.anchorKind !== kind) return false;
721
- return !filter.pagePath || thread.anchor && !("type" in thread.anchor) && thread.anchor.pagePath === filter.pagePath;
722
- });
723
- return output(() => parseThreadSnapshot({
724
- connection: {
725
- state: "ready",
726
- adapter: store.schema.adapter
727
- },
728
- capabilities: store.capabilities,
729
- threads: threads.slice(0, MAX_THREADS).map((thread) => view(thread, true)),
730
- truncated: threads.length > MAX_THREADS
731
- }));
732
- }
733
- function link(root, id, ids, expectedRevision, remove) {
734
- return store.run(root, true, (data, at) => {
735
- if (!store.capabilities.link) refused("This adapter does not allow linking discussions.");
736
- let request = input$3(() => parseThreadLinkInput({
737
- itemIds: ids,
738
- expectedRevision
739
- }));
740
- let thread = store.thread(data, id);
741
- expected(thread, request.expectedRevision);
742
- for (let id of request.itemIds) store.item(data, id);
743
- let itemIds = remove ? thread.itemIds.filter((id) => !request.itemIds.includes(id)) : [.../* @__PURE__ */ new Set([...thread.itemIds, ...request.itemIds])];
744
- if (itemIds.length > 100) refused("This discussion has too many linked work items.");
745
- if (!thread.anchor && !itemIds.length) refused("An unanchored discussion needs a linked work item.");
746
- return save(data, {
747
- ...thread,
748
- itemIds
749
- }, at, thread.itemIds);
750
- });
751
- }
752
- return {
753
- watch: (root) => store.watch(root),
754
- list: (root, value) => store.run(root, false, (data) => snapshot(data, input$3(() => parseThreadFilter(value)))),
755
- get: (root, id) => store.run(root, false, (data) => view(store.thread(data, id))),
756
- create: (root, value, author) => store.run(root, true, (data, at) => {
757
- if (!store.capabilities.create) refused("This adapter does not allow creating discussions.");
758
- let request = input$3(() => parseThreadInput(value));
759
- let trusted = input$3(() => parseMessageAuthor(author));
760
- for (let id of request.itemIds) store.item(data, id);
761
- return save(data, {
762
- id: store.next(data, "thread"),
763
- revision: null,
764
- status: "open",
765
- anchor: request.anchor,
766
- itemIds: request.itemIds,
767
- messages: [{
768
- id: store.next(data, "message"),
769
- body: request.body,
770
- author: trusted,
771
- createdAt: at,
772
- parentId: null
773
- }],
774
- messageCount: 1,
775
- messagesLoaded: true,
776
- messagesTruncated: false,
777
- createdAt: at,
778
- updatedAt: at
779
- }, at);
780
- }),
781
- reply: (root, id, value, author) => store.run(root, true, (data, at) => {
782
- if (!store.capabilities.reply) refused("This adapter does not allow replying to discussions.");
783
- let request = input$3(() => parseThreadReplyInput(value));
784
- let trusted = input$3(() => parseMessageAuthor(author));
785
- let thread = store.thread(data, id);
786
- expected(thread, request.expectedRevision);
787
- if (request.parentId && !thread.messages.some((message) => message.id === request.parentId)) refused("The reply parent must belong to this discussion.");
788
- return save(data, {
789
- ...thread,
790
- messages: [...thread.messages, {
791
- id: store.next(data, "message"),
792
- body: request.body,
793
- author: trusted,
794
- createdAt: at,
795
- parentId: request.parentId
796
- }]
797
- }, at);
798
- }),
799
- setStatus: (root, id, status, expectedRevision) => store.run(root, true, (data, at) => {
800
- if (!store.capabilities.status) refused("This adapter does not allow changing discussion status.");
801
- let request = input$3(() => parseThreadStatusInput({
802
- status,
803
- expectedRevision
804
- }));
805
- let thread = store.thread(data, id);
806
- expected(thread, request.expectedRevision);
807
- return save(data, {
808
- ...thread,
809
- status: request.status
810
- }, at);
811
- }),
812
- deleteMessage: (root, id, value, author) => store.run(root, true, (data, at) => {
813
- if (!store.capabilities.delete) refused("This adapter does not allow deleting messages.");
814
- let request = input$3(() => parseThreadDeleteInput(value));
815
- let trusted = input$3(() => parseMessageAuthor(author));
816
- let thread = store.thread(data, id);
817
- expected(thread, request.expectedRevision);
818
- let refusal = messageDeletionRefusal(thread.messages, request.messageId, trusted.id);
819
- if (refusal === "missing") throw new WorkError({
820
- reason: "not-found",
821
- message: messageDeletionRefusals.missing
822
- });
823
- if (refusal) refused(messageDeletionRefusals[refusal]);
824
- if (thread.messages[0].id === request.messageId) {
825
- data.threads.delete(id);
826
- store.touchThreads(data, thread.itemIds, at);
827
- return null;
828
- }
829
- return save(data, {
830
- ...thread,
831
- messages: withoutMessages(thread.messages, [request.messageId])
832
- }, at);
833
- }),
834
- link: (root, id, ids, revision) => link(root, id, ids, revision, false),
835
- unlink: (root, id, ids, revision) => link(root, id, ids, revision, true)
836
- };
837
- }
838
- //#endregion
839
- //#region ../work/src/server/memory-actions.ts
840
- /**
841
- * Computes what a declared action changes on an item, using its custom implementation or the
842
- * built-in status, claim and relation behaviour.
843
- * @param store - The memory store, with its schema, author and action implementations.
844
- * @param item - The item the action runs on.
845
- * @param action - The action's declaration.
846
- * @param values - The action's validated input.
847
- * @param at - When the action runs, for close timestamps.
848
- * @returns The new values or outgoing relations.
849
- */
850
- function actionChange(store, item, action, values, at) {
851
- let custom = store.options.actions?.[action.id];
852
- if (custom) return custom(structuredClone({
853
- item,
854
- input: values,
855
- schema: store.schema,
856
- author: store.author
857
- }));
858
- let patch = {};
859
- let status = fieldByRole(store.schema, "status");
860
- if (action.kind === "close" || action.kind === "reopen" || action.kind === "transition") {
861
- if (!status) refused("This schema has no status field.");
862
- let destination = action.kind === "transition" ? values[action.input?.find((field) => field.role === "status" || field.kind === "status")?.id ?? ""] : store.schema.statuses.find((status) => action.kind === "close" ? status.category === "done" : ["unstarted", "backlog"].includes(status.category))?.value;
863
- if (typeof destination !== "string" || !store.schema.statuses.some((status) => status.value === destination)) refused("This action has no valid destination status.");
864
- patch[status.id] = destination;
865
- let closed = fieldByRole(store.schema, "closed");
866
- if (closed && action.kind === "close") patch[closed.id] = at;
867
- if (closed && !closed.required && action.kind === "reopen") patch[closed.id] = null;
868
- let reason = fieldByRole(store.schema, "closeReason");
869
- let reasonInput = action.input?.find((field) => field.role === "closeReason");
870
- if (reason && reasonInput && values[reasonInput.id] !== void 0) patch[reason.id] = values[reasonInput.id];
871
- return { values: patch };
872
- }
873
- if (action.kind === "claim" || action.kind === "unclaim") {
874
- let assignee = fieldByRole(store.schema, "assignee");
875
- if (!assignee) refused("This schema has no assignee field.");
876
- let current = item.values[assignee.id];
877
- if (action.kind === "claim" && current !== null && current !== void 0 && current !== "" && !(Array.isArray(current) && !current.length) && !(typeof current === "string" && current === store.author.name) && !(typeof current === "object" && !Array.isArray(current) && current.id === store.author.id)) refused("This work item is already assigned.");
878
- let person = {
879
- id: store.author.id,
880
- name: store.author.name,
881
- email: store.author.email
882
- };
883
- let claimed = action.kind === "claim";
884
- if (assignee.kind === "person") patch[assignee.id] = claimed ? person : null;
885
- else if (assignee.kind === "people") patch[assignee.id] = claimed ? [person] : [];
886
- else if (assignee.kind === "text") patch[assignee.id] = claimed ? person.name : "";
887
- else refused("This assignee field cannot represent a claim.");
888
- return { values: patch };
889
- }
890
- if (action.kind === "relate" || action.kind === "unrelate") {
891
- let targetField = action.input?.find((field) => field.kind === "item");
892
- if (!targetField) refused("A relation action requires an item input.");
893
- let target = input$3(() => parseWorkItemRef(values[targetField.id]));
894
- let editable = store.schema.relations.filter((relation) => relation.editable);
895
- let relationField = action.input?.find((field) => field.kind === "enum");
896
- let type = relationField ? values[relationField.id] : editable.length === 1 ? editable[0].type : void 0;
897
- if (typeof type !== "string" || !editable.some((relation) => relation.type === type)) refused("Select an editable relation type.");
898
- let remaining = item.relations.filter((relation) => !(relation.direction === "out" && relation.type === type && relation.item.id === target.id));
899
- return { relations: action.kind === "unrelate" ? remaining : [...remaining, {
900
- type,
901
- direction: "out",
902
- item: target
903
- }] };
904
- }
905
- refused("This action needs an implementation.");
906
- }
907
- function reference(store, item) {
908
- let title = valueByRole(item, store.schema, "title");
909
- let status = valueByRole(item, store.schema, "status");
910
- return {
911
- id: item.id,
912
- ...typeof title === "string" ? { title: title.slice(0, 1e3) } : {},
913
- ...typeof status === "string" ? { status } : {}
914
- };
915
- }
916
- /**
917
- * Rebuilds every item's incoming relations and link summaries from the outgoing ones, refusing
918
- * self-links and parent or blocking cycles, and saves the items that changed.
919
- * @param store - The memory store.
920
- * @param data - The project's items.
921
- * @param at - When the change happens.
922
- */
923
- function reconcileRelations(store, data, at) {
924
- let incoming = /* @__PURE__ */ new Map();
925
- for (let row of data.items.values()) for (let relation of row.item.relations.filter((relation) => relation.direction === "out")) {
926
- store.item(data, relation.item.id);
927
- if (relation.item.id === row.item.id) refused("A work item cannot relate to itself.");
928
- let relations = incoming.get(relation.item.id) ?? [];
929
- relations.push({
930
- type: relation.type,
931
- direction: "in",
932
- item: reference(store, row.item)
933
- });
934
- incoming.set(relation.item.id, relations);
935
- let kind = store.schema.relations.find((kind) => kind.type === relation.type)?.kind;
936
- if (kind !== "parent" && kind !== "blocks") continue;
937
- let pending = [relation.item.id];
938
- let seen = /* @__PURE__ */ new Set();
939
- while (pending.length) {
940
- let id = pending.pop();
941
- if (id === row.item.id) refused("This relationship would create a cycle.");
942
- if (seen.has(id)) continue;
943
- seen.add(id);
944
- for (let edge of store.item(data, id).item.relations) if (edge.direction === "out" && edge.type === relation.type) pending.push(edge.item.id);
945
- }
946
- }
947
- for (let row of data.items.values()) {
948
- let relations = [...row.item.relations.filter((relation) => relation.direction === "out").map((relation) => ({
949
- ...relation,
950
- item: reference(store, store.item(data, relation.item.id).item)
951
- })), ...incoming.get(row.item.id) ?? []];
952
- if (JSON.stringify(row.item.relations) !== JSON.stringify(relations)) store.save(data, {
953
- ...row.item,
954
- relations
955
- }, at, "Relationships changed");
956
- }
957
- }
958
- //#endregion
959
- //#region ../work/src/server/memory-work.ts
960
- var actionResult = strict({
377
+ nonempty(256);
378
+ nonempty(4096);
379
+ strict({
961
380
  values: any(),
962
381
  relations: any()
963
382
  });
964
- /**
965
- * The work adapter over an in-memory store.
966
- * @param store - The store shared with the thread adapter.
967
- * @returns The `WorkAdapter` implementation.
968
- */
969
- function memoryWork(store) {
970
- let schema = store.schema;
971
- let connection = {
972
- state: "ready",
973
- adapter: schema.adapter
974
- };
975
- function detail(data, id) {
976
- let row = store.item(data, id);
977
- let messages = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).flatMap((thread) => thread.messages);
978
- return output(() => parseWorkDetail({
979
- connection,
980
- item: store.view(data, id),
981
- discussion: schema.capabilities.discussion ? available(messages, 200) : unsupported$1(),
982
- history: schema.capabilities.history ? available(row.history, 50, row.historyCount) : unsupported$1(),
983
- activity: schema.capabilities.activity ? available(row.activity, 200, row.activityCount) : unsupported$1()
984
- }, schema));
985
- }
986
- function result(data, id) {
987
- return output(() => parseWorkMutationResult({ item: store.view(data, id) }, schema));
988
- }
989
- return {
990
- watch: (root) => store.watch(root),
991
- describe: (root) => store.run(root, false, () => connection),
992
- schema: (root) => store.run(root, false, () => schema),
993
- list: (root, value) => store.run(root, false, (data) => {
994
- let query = input$3(() => parseWorkQuery(value));
995
- let search = query.search?.trim().toLowerCase();
996
- let rows = [...data.items.values()].map((row) => row.item).filter((item) => {
997
- if (query.status && valueByRole(item, schema, "status") !== query.status) return false;
998
- return !search || [item.id, ...Object.values(item.values).filter((value) => typeof value === "string")].some((value) => value.toLowerCase().includes(search));
999
- });
1000
- let items = rows.slice(0, query.limit).map((row) => {
1001
- let item = store.view(data, row.id);
1002
- return {
1003
- ...item,
1004
- values: Object.fromEntries(Object.entries(item.values).filter(([key]) => schema.fields.find((field) => field.id === key)?.kind !== "markdown")),
1005
- contentLoaded: false
1006
- };
1007
- });
1008
- return output(() => parseWorkSnapshot({
1009
- connection,
1010
- items,
1011
- truncated: rows.length > query.limit,
1012
- schemaVersion: schema.adapter.version
1013
- }, schema));
1014
- }),
1015
- get: (root, id) => store.run(root, false, (data) => detail(data, id)),
1016
- section: (root, id, sectionId) => store.run(root, false, (data) => {
1017
- input$3(() => parseWork(identifier, sectionId, "sectionId"));
1018
- let item = store.view(data, id);
1019
- let declared = schema.sections.find((section) => section.id === sectionId);
1020
- if (!declared) return {
1021
- kind: "custom",
1022
- data: unsupported$1()
1023
- };
1024
- let custom = store.options.sections?.[sectionId];
1025
- let result;
1026
- if (custom) result = custom(structuredClone(item));
1027
- else if (declared.kind === "activity") result = {
1028
- kind: "activity",
1029
- data: schema.capabilities.activity ? available(store.item(data, id).activity, 200, store.item(data, id).activityCount) : unsupported$1()
1030
- };
1031
- else if (declared.kind === "history") result = {
1032
- kind: "history",
1033
- data: schema.capabilities.history ? available(store.item(data, id).history, 50, store.item(data, id).historyCount) : unsupported$1()
1034
- };
1035
- else if (declared.kind === "metadata") result = {
1036
- kind: "metadata",
1037
- data: available([item.extra], 1)
1038
- };
1039
- else result = {
1040
- kind: declared.kind,
1041
- data: unsupported$1()
1042
- };
1043
- return output(() => {
1044
- if (result.kind !== declared.kind) throw new TypeError("The section renderer changed.");
1045
- return parseSectionData(result, schema);
1046
- });
1047
- }),
1048
- create: (root, value) => store.run(root, true, (data, at) => {
1049
- if (!schema.capabilities.create) refused("This adapter does not allow creating work items.");
1050
- let request = input$3(() => parseWorkCreate(value, schema));
1051
- let id = store.next(data, "work");
1052
- store.save(data, {
1053
- id,
1054
- revision: null,
1055
- values: store.initialValues(id, at, request.values),
1056
- relations: [],
1057
- counts: {},
1058
- contentLoaded: true,
1059
- extra: null
1060
- }, at, "Work item created");
1061
- return result(data, id);
1062
- }),
1063
- update: (root, id, patch, expectedRevision) => store.run(root, true, (data, at) => {
1064
- if (!schema.capabilities.update) refused("This adapter does not allow editing work items.");
1065
- let request = input$3(() => parseWorkUpdate({
1066
- values: patch,
1067
- expectedRevision
1068
- }, schema));
1069
- let item = store.item(data, id).item;
1070
- expected(item, request.expectedRevision);
1071
- store.save(data, {
1072
- ...item,
1073
- values: {
1074
- ...item.values,
1075
- ...request.values
1076
- }
1077
- }, at, "Work item updated");
1078
- reconcileRelations(store, data, at);
1079
- return result(data, id);
1080
- }),
1081
- act: (root, id, actionId, values, expectedRevision) => store.run(root, true, (data, at) => {
1082
- let request = input$3(() => parseWorkActionInput({
1083
- action: actionId,
1084
- input: values,
1085
- expectedRevision
1086
- }, schema));
1087
- let item = store.item(data, id).item;
1088
- expected(item, request.expectedRevision);
1089
- let action = schema.actions.find((action) => action.id === request.action);
1090
- let changed = actionChange(store, item, action, request.input, at);
1091
- output(() => parseWork(actionResult, changed, "actionResult"));
1092
- let next = {
1093
- ...item,
1094
- values: {
1095
- ...item.values,
1096
- ...changed.values
1097
- },
1098
- relations: changed.relations ?? item.relations
1099
- };
1100
- store.save(data, next, at, action.label);
1101
- reconcileRelations(store, data, at);
1102
- return result(data, id);
1103
- })
1104
- };
1105
- }
1106
- //#endregion
1107
- //#region ../work/src/server/memory.ts
1108
- /**
1109
- * Creates isolated work and discussion services for each layer and project root.
1110
- * @param options - The schema, complete seed records and trusted behavior for this adapter.
1111
- * @returns A layer with atomic in-memory writes and no filesystem or process requirements.
1112
- */
1113
- function memoryAdapterLayer(options) {
1114
- return effectContext(try_({
1115
- try: () => {
1116
- let store = new MemoryStore(options);
1117
- return make$1(WorkAdapter, memoryWork(store)).pipe(add(ThreadAdapter, memoryThreads(store)));
1118
- },
1119
- catch: (cause) => new WorkError({
1120
- reason: "input",
1121
- message: "The memory adapter configuration is invalid.",
1122
- cause
1123
- })
1124
- }));
1125
- }
1126
383
  //#endregion
1127
384
  //#region app/server/adapter-definition.ts
1128
385
  /**
@@ -1146,24 +403,8 @@ function defineAdapter(definition) {
1146
403
  }
1147
404
  //#endregion
1148
405
  //#region app/server/adapters.ts
1149
- /** Invalid modules fail before a companion accepts requests. Causes stay on the server. */
406
+ /** A store or design that fails before the companion serves; causes stay on the server. */
1150
407
  var AdapterStartupError = class extends TaggedError("AdapterStartupError") {};
1151
- /** Module evaluation is replaceable in tests and cached by the registry, not globally. */
1152
- var AdapterModuleLoader = class extends Service()("bud/AdapterModuleLoader") {};
1153
- var adapterModuleLoaderLayer = succeed$1(AdapterModuleLoader, { load: (filename) => tryPromise({
1154
- try: async () => {
1155
- let url = pathToFileURL(filename);
1156
- url.searchParams.set("bud", randomUUID());
1157
- return (await import(
1158
- /* @vite-ignore */
1159
- url.href
1160
- )).default;
1161
- },
1162
- catch: (cause) => new AdapterStartupError({
1163
- message: `Could not evaluate adapter module ${filename}.`,
1164
- cause
1165
- })
1166
- }) });
1167
408
  /** The project's selected work and thread stores, its storage settings and its task design. */
1168
409
  var AdapterRegistry = class extends Service()("bud/AdapterRegistry") {};
1169
410
  function failure$3(message) {
@@ -1172,10 +413,10 @@ function failure$3(message) {
1172
413
  cause
1173
414
  });
1174
415
  }
1175
- function moduleFile(root, module, setting) {
416
+ function designFile(root, module) {
1176
417
  return projectModuleFile(root, module, {
1177
- setting,
1178
- kind: setting.startsWith("storage.") ? "server" : "browser"
418
+ setting: "taskDesign",
419
+ kind: "browser"
1179
420
  }).pipe(mapError((cause) => new AdapterStartupError({
1180
421
  message: cause.message,
1181
422
  cause
@@ -1204,35 +445,34 @@ function publicConnection(definition, connection) {
1204
445
  * @param project - The trusted project context, updated with public connection state before serving
1205
446
  * it.
1206
447
  * @param builtIns - Explicit built-in implementations supplied by application composition.
1207
- * @returns The registry layer; module loading and all acquired resources share its lifetime.
448
+ * @returns The registry layer; all acquired resources share its lifetime.
1208
449
  */
1209
450
  function adapterRegistryLayer(project, builtIns) {
1210
451
  return unwrap(gen(function* () {
1211
452
  let root = project.root;
1212
- let loader = yield* AdapterModuleLoader;
1213
453
  let loaded = /* @__PURE__ */ new Map();
1214
454
  let selected = {
1215
455
  work: null,
1216
456
  threads: null
1217
457
  };
1218
458
  for (let kind of ["work", "threads"]) {
1219
- let module = project.config.storage[kind];
1220
- if (module === false) continue;
1221
- let key = module.startsWith("bud:") ? module : yield* moduleFile(root, module, `storage.${kind}`);
1222
- let definition = loaded.get(key);
459
+ let name = project.config.storage[kind];
460
+ if (name === false) continue;
461
+ let definition = loaded.get(name);
1223
462
  if (!definition) {
1224
- let value = module.startsWith("bud:") ? builtIns[module] : yield* loader.load(key);
463
+ let value = builtIns[name];
464
+ if (!value) return yield* new AdapterStartupError({ message: `No plugin provides the ${kind === "work" ? "task" : "discussion"} store ${name}.` });
1225
465
  definition = yield* try_({
1226
466
  try: () => defineAdapter(value),
1227
- catch: failure$3(`storage.${kind} adapter ${module} must export a valid adapter definition.`)
467
+ catch: failure$3(`The ${kind} store ${name} is not a valid adapter definition.`)
1228
468
  });
1229
- loaded.set(key, definition);
469
+ loaded.set(name, definition);
1230
470
  }
1231
- if (!definition[kind]) return yield* new AdapterStartupError({ message: `storage.${kind} adapter ${definition.id} does not provide ${kind}.` });
471
+ if (!definition[kind]) return yield* new AdapterStartupError({ message: `The ${kind === "work" ? "task" : "discussion"} store ${definition.id} does not provide ${kind}.` });
1232
472
  selected[kind] = definition;
1233
473
  }
1234
474
  let design = project.config.designs.tasks;
1235
- let taskDesignFile = design === "bud:beads" ? null : yield* moduleFile(root, design, "designs.tasks");
475
+ let taskDesignFile = design === "bud:beads" ? null : yield* designFile(root, design);
1236
476
  let workLayer = selected.work?.work;
1237
477
  let threadLayer = selected.threads?.threads;
1238
478
  let layers = mergeAll(workLayer ? provide$1(effect(WorkAdapter, gen(function* () {
@@ -1265,7 +505,13 @@ function adapterRegistryLayer(project, builtIns) {
1265
505
  "create",
1266
506
  "update",
1267
507
  "act"
1268
- ], ["sync", "read"]);
508
+ ], [
509
+ "sync",
510
+ "read",
511
+ "addStatuses",
512
+ "readItemData",
513
+ "writeItemData"
514
+ ]);
1269
515
  let connection = yield* work.describe(root).pipe(flatMap((value) => try_({
1270
516
  try: () => parseWorkConnection(value),
1271
517
  catch: failure$3(`Adapter ${selected.work.id} returned an invalid work connection.`)
@@ -1315,6 +561,75 @@ function adapterRegistryLayer(project, builtIns) {
1315
561
  }));
1316
562
  }
1317
563
  //#endregion
564
+ //#region app/server/attachments.ts
565
+ /** Images that belong to tasks, such as a comment's screenshot, kept under their content's hash. */
566
+ var Attachments = class extends Service()("bud/Attachments") {};
567
+ var hashPattern = /^[a-f0-9]{64}$/;
568
+ /**
569
+ * Whether bytes are a JPEG of a size a task keeps.
570
+ * @param bytes - The upload.
571
+ * @returns Whether it is acceptable.
572
+ */
573
+ function acceptableAttachment(bytes) {
574
+ return bytes.length > 3 && bytes.length <= 2097152 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
575
+ }
576
+ async function sha256(bytes) {
577
+ let digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
578
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
579
+ }
580
+ /**
581
+ * Attachments in a store, under `attachments/<hash>.jpg`.
582
+ * @param store - The directory or host store.
583
+ * @returns The attachments layer.
584
+ */
585
+ function attachmentsLayer(store) {
586
+ let failed = (message) => (cause) => new WorkError({
587
+ reason: "storage",
588
+ message,
589
+ cause
590
+ });
591
+ return succeed$1(Attachments, {
592
+ put: (bytes) => gen(function* () {
593
+ if (!acceptableAttachment(bytes)) return yield* new WorkError({
594
+ reason: "input",
595
+ message: "Attachments are JPEG images of at most 2 MB."
596
+ });
597
+ let hash = yield* tryPromise({
598
+ try: () => sha256(bytes),
599
+ catch: failed("Could not read the image.")
600
+ });
601
+ yield* tryPromise({
602
+ try: () => store.write(`attachments/${hash}.jpg`, bytes),
603
+ catch: failed("Could not keep the image.")
604
+ });
605
+ return hash;
606
+ }),
607
+ get: (hash) => hashPattern.test(hash) ? tryPromise({
608
+ try: async () => await store.read(`attachments/${hash}.jpg`) ?? null,
609
+ catch: failed("Could not read the image.")
610
+ }) : succeed(null)
611
+ });
612
+ }
613
+ /**
614
+ * A directory as an attachment store, such as the project's cache.
615
+ * @param directory - Where the files go.
616
+ * @returns The store.
617
+ */
618
+ function directoryAttachmentStore(directory) {
619
+ return {
620
+ async read(key) {
621
+ return readFile(join(directory, key)).then((bytes) => new Uint8Array(bytes), () => void 0);
622
+ },
623
+ async write(key, bytes) {
624
+ let path = join(directory, key);
625
+ await mkdir(dirname(path), { recursive: true });
626
+ let staged = `${path}.${process.pid}.tmp`;
627
+ await writeFile(staged, bytes);
628
+ await rename(staged, path);
629
+ }
630
+ };
631
+ }
632
+ //#endregion
1318
633
  //#region ../live/src/server/hub.ts
1319
634
  /**
1320
635
  * Orders every published change into one stream per companion. Clients resume from their
@@ -1646,7 +961,7 @@ function publishDiffs(store, read, diff, external, quiet) {
1646
961
  pending = true;
1647
962
  changes++;
1648
963
  })));
1649
- yield* merge(make$2(void 0), merge(changed, activity.requests)).pipe(debounce(quiet), runForEach(() => pending ? andThen(sync(() => {
964
+ yield* merge(make$1(void 0), merge(changed, activity.requests)).pipe(debounce(quiet), runForEach(() => pending ? andThen(sync(() => {
1650
965
  pending = false;
1651
966
  }), check) : void_$1), forkScoped);
1652
967
  });
@@ -1661,13 +976,1426 @@ function publishDiffs(store, read, diff, external, quiet) {
1661
976
  function storeLiveLayer(root) {
1662
977
  return effectDiscard(gen(function* () {
1663
978
  let registry = yield* AdapterRegistry;
1664
- let reads = yield* make$3(1);
979
+ let reads = yield* make$2(1);
1665
980
  yield* publishDiffs("work", reads.withPermit(readWorkspace(root, parseWorkQuery({}))), diffWorkspaces, registry.work?.watch?.(root) ?? empty$1, "250 millis");
1666
981
  let threads = registry.threads;
1667
982
  if (threads) yield* publishDiffs("threads", reads.withPermit(threads.list(root, {}).pipe(map(parseThreadSnapshot))), diffThreads, threads.watch?.(root) ?? empty$1, "400 millis");
1668
983
  }));
1669
984
  }
1670
985
  //#endregion
986
+ //#region ../factory/src/server/errors.ts
987
+ var RunErrorBase = TaggedError("RunError");
988
+ /** A sandbox or agent failure; `retryable` when trying the same step again may work. */
989
+ var RunError = class extends RunErrorBase {};
990
+ //#endregion
991
+ //#region ../factory/src/server/executor.ts
992
+ /** The project's run executor, or null when it has no sandbox to run in. */
993
+ var RunExecutor = class extends Service()("@bud.sh/factory/RunExecutor") {};
994
+ //#endregion
995
+ //#region ../factory/src/server/agent-token.ts
996
+ var PURPOSE = "bud-tasks-read";
997
+ function sign(secret, body) {
998
+ return createHmac("sha256", secret).update(`${PURPOSE}:${body}`).digest("base64url");
999
+ }
1000
+ /**
1001
+ * The token a run's agent reads the task database with: its task and run, signed.
1002
+ * @param secret - The server's agent secret.
1003
+ * @param run - The run's id and its task's.
1004
+ * @returns The token.
1005
+ */
1006
+ function agentToken(secret, run) {
1007
+ let body = Buffer.from(JSON.stringify([run.taskId, run.id])).toString("base64url");
1008
+ return `${body}.${sign(secret, body)}`;
1009
+ }
1010
+ /**
1011
+ * Reads a token an agent sent, if this server signed it.
1012
+ * @param secret - The server's agent secret.
1013
+ * @param token - What the agent sent.
1014
+ * @returns Its task and run, or null for a token this server didn't sign.
1015
+ */
1016
+ function readAgentToken(secret, token) {
1017
+ let [body, signature, ...rest] = token.split(".");
1018
+ if (!body || !signature || rest.length || token.length > 2e3) return null;
1019
+ let expected = Buffer.from(sign(secret, body));
1020
+ let given = Buffer.from(signature);
1021
+ if (expected.length !== given.length || !timingSafeEqual(expected, given)) return null;
1022
+ try {
1023
+ let value = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
1024
+ if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "string" || typeof value[1] !== "string") return null;
1025
+ return {
1026
+ taskId: value[0],
1027
+ runId: value[1]
1028
+ };
1029
+ } catch {
1030
+ return null;
1031
+ }
1032
+ }
1033
+ //#endregion
1034
+ //#region ../factory/src/server/runs.ts
1035
+ var MAX_ATTEMPTS = 3;
1036
+ /**
1037
+ * Runs that can't start, for a project whose factory is off or whose store can't keep runs.
1038
+ * @param message - Why, as Start says it.
1039
+ * @returns Runs that refuse every action.
1040
+ */
1041
+ function noRuns(message) {
1042
+ let no = fail$1(refused(message));
1043
+ return {
1044
+ get: () => succeed(null),
1045
+ start: () => no,
1046
+ changes: () => no,
1047
+ answer: () => no,
1048
+ approve: () => no,
1049
+ discard: () => no,
1050
+ retry: () => no,
1051
+ preview: () => succeed(null),
1052
+ terminal: () => succeed(null),
1053
+ touch: () => void_$1,
1054
+ agentTask: () => succeed(null),
1055
+ wake: () => succeed(null),
1056
+ checkMerged: () => void_$1,
1057
+ resume: void_$1
1058
+ };
1059
+ }
1060
+ function refused(message) {
1061
+ return new WorkError({
1062
+ reason: "refused",
1063
+ message
1064
+ });
1065
+ }
1066
+ /**
1067
+ * A project's runs, with background work tied to the surrounding scope.
1068
+ * @param options - The project, its task store, the executor and how to tell views about changes.
1069
+ * @returns The runs; their background fibers stop with the scope, the sandboxes don't.
1070
+ */
1071
+ function makeRuns(options) {
1072
+ return gen(function* () {
1073
+ let scope$2 = yield* scope;
1074
+ let { root, factory, work, executor } = options;
1075
+ let now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1076
+ let newId = options.id ?? (() => crypto.randomUUID());
1077
+ let locks = /* @__PURE__ */ new Map();
1078
+ let following = /* @__PURE__ */ new Set();
1079
+ let sleepAfter = options.sleepAfterMs === void 0 ? 3e5 : options.sleepAfterMs;
1080
+ let dozes = /* @__PURE__ */ new Map();
1081
+ let wakeLocks = /* @__PURE__ */ new Map();
1082
+ let underway = /* @__PURE__ */ new Set();
1083
+ let owned = (runId, step) => suspend(() => {
1084
+ if (underway.has(runId)) return succeed(null);
1085
+ underway.add(runId);
1086
+ return step.pipe(ensuring(sync(() => underway.delete(runId))));
1087
+ });
1088
+ let readData = work.readItemData;
1089
+ let writeData = work.writeItemData;
1090
+ if (!readData || !writeData) return noRuns("The task store can’t keep runs; it needs item data support.");
1091
+ let readItem = readData;
1092
+ let writeItem = writeData;
1093
+ function lock(taskId) {
1094
+ let found = locks.get(taskId);
1095
+ if (!found) locks.set(taskId, found = makeUnsafe$1(1));
1096
+ return found;
1097
+ }
1098
+ function background(effect) {
1099
+ return asVoid(forkIn(ignore(effect), scope$2));
1100
+ }
1101
+ let read = (taskId) => readItem(root, taskId, "budRun").pipe(flatMap(({ value, revision }) => try_({
1102
+ try: () => ({
1103
+ run: value === null ? null : parseRunRecord(value),
1104
+ revision
1105
+ }),
1106
+ catch: (cause) => new WorkError({
1107
+ reason: "storage",
1108
+ message: "The task’s run is unreadable.",
1109
+ cause
1110
+ })
1111
+ })));
1112
+ function schema() {
1113
+ return flatMap(work.schema(root), (value) => try_({
1114
+ try: () => parseWorkSchema(value),
1115
+ catch: (cause) => new WorkError({
1116
+ reason: "storage",
1117
+ message: "Invalid task schema.",
1118
+ cause
1119
+ })
1120
+ }));
1121
+ }
1122
+ function setStatus(taskId, status, revision) {
1123
+ return gen(function* () {
1124
+ let current = yield* schema();
1125
+ let field = fieldByRole(current, "status");
1126
+ if (!field || !revision) return;
1127
+ let values = { [field.id]: factory.statuses[status] };
1128
+ yield* work.update(root, taskId, values, revision).pipe(catchIf((error) => error.reason === "conflict", () => flatMap(read(taskId), (fresh) => fresh.revision ? work.update(root, taskId, values, fresh.revision) : void_$1)));
1129
+ });
1130
+ }
1131
+ /**
1132
+ * Reads the run, decides the step and saves it, again if the task changed meanwhile.
1133
+ * @param taskId - The task.
1134
+ * @param decide - The step from the stored run.
1135
+ * @returns The saved step.
1136
+ */
1137
+ function transact(taskId, decide) {
1138
+ return lock(taskId).withPermit(gen(function* () {
1139
+ for (let attempt = 1;; attempt++) {
1140
+ let { run, revision } = yield* read(taskId);
1141
+ if (!revision) return yield* new WorkError({
1142
+ reason: "storage",
1143
+ message: "The task has no revision."
1144
+ });
1145
+ let step = yield* try_({
1146
+ try: () => decide(run),
1147
+ catch: (cause) => cause instanceof RunTransitionError ? refused(cause.message) : new WorkError({
1148
+ reason: "input",
1149
+ message: "Invalid run step.",
1150
+ cause
1151
+ })
1152
+ });
1153
+ let saved = yield* writeItem(root, taskId, "budRun", step.run, revision).pipe(map((result) => result.revision), catchIf((error) => error.reason === "conflict" && attempt < MAX_ATTEMPTS, () => void_$1));
1154
+ if (saved === void 0) continue;
1155
+ if (step.status) yield* setStatus(taskId, step.status, saved);
1156
+ yield* options.changed;
1157
+ return step;
1158
+ }
1159
+ }));
1160
+ }
1161
+ function report(taskId, event) {
1162
+ return transact(taskId, (run) => {
1163
+ if (!run) throw new RunTransitionError(null, event.type);
1164
+ return advanceRun(run, event, now());
1165
+ }).pipe(flatMap((step) => perform(taskId, step.effects)));
1166
+ }
1167
+ function fail(taskId, error) {
1168
+ return report(taskId, {
1169
+ type: "fail",
1170
+ message: error.message,
1171
+ retryable: error.retryable
1172
+ });
1173
+ }
1174
+ function task(taskId) {
1175
+ return gen(function* () {
1176
+ let current = yield* schema();
1177
+ let detail = yield* flatMap(work.get(root, taskId), (value) => try_({
1178
+ try: () => parseWorkDetail(value, current),
1179
+ catch: (cause) => new WorkError({
1180
+ reason: "storage",
1181
+ message: "Invalid task.",
1182
+ cause
1183
+ })
1184
+ }));
1185
+ let text = (role) => {
1186
+ let value = valueByRole(detail.item, current, role);
1187
+ return typeof value === "string" ? value : "";
1188
+ };
1189
+ let images = options.images ? yield* options.images(taskId) : [];
1190
+ return {
1191
+ id: taskId,
1192
+ title: text("title") || taskId,
1193
+ description: text("description"),
1194
+ ...images.length ? { images } : {}
1195
+ };
1196
+ });
1197
+ }
1198
+ function follow(taskId, run) {
1199
+ if (!executor || following.has(run.id)) return void_$1;
1200
+ following.add(run.id);
1201
+ return background(executor.follow(run).pipe(runForEach((event) => report(taskId, event)), catchTag("RunError", (error) => fail(taskId, error)), ensuring(sync(() => following.delete(run.id)))));
1202
+ }
1203
+ function provision(taskId, runId) {
1204
+ if (underway.has(runId)) return void_$1;
1205
+ underway.add(runId);
1206
+ return gen(function* () {
1207
+ if (!executor) return;
1208
+ let { run } = yield* read(taskId);
1209
+ if (!run || run.id !== runId || run.state !== "provisioning") return;
1210
+ let started = run;
1211
+ yield* report(taskId, {
1212
+ type: "ready",
1213
+ ...yield* executor.provision(started, yield* task(taskId), (sandbox) => ignore(report(taskId, {
1214
+ type: "created",
1215
+ sandbox
1216
+ })))
1217
+ });
1218
+ let { run: working } = yield* read(taskId);
1219
+ if (working?.state === "working") yield* follow(taskId, working);
1220
+ }).pipe(catchTag("RunError", (error) => fail(taskId, error)), ensuring(sync(() => underway.delete(runId))));
1221
+ }
1222
+ function push(taskId) {
1223
+ return gen(function* () {
1224
+ if (!executor) return;
1225
+ let { run } = yield* read(taskId);
1226
+ if (!run || run.state !== "pushing") return;
1227
+ if (run.asleep) {
1228
+ yield* wake(taskId).pipe(mapError((cause) => new RunError({
1229
+ message: cause.message,
1230
+ retryable: true
1231
+ })));
1232
+ run = (yield* read(taskId)).run ?? run;
1233
+ }
1234
+ let commit = yield* owned(run.id, executor.push(run));
1235
+ if (!commit) return;
1236
+ yield* report(taskId, {
1237
+ type: "pushed",
1238
+ commit
1239
+ });
1240
+ let { run: pushed } = yield* read(taskId);
1241
+ if (pushed?.state === "pushed" && options.onPushed) yield* ignore(options.onPushed(pushed));
1242
+ }).pipe(catchTag("RunError", (error) => fail(taskId, error)));
1243
+ }
1244
+ function doze(taskId) {
1245
+ if (!executor || sleepAfter === null) return void_$1;
1246
+ let generation = (dozes.get(taskId) ?? 0) + 1;
1247
+ dozes.set(taskId, generation);
1248
+ return background(gen(function* () {
1249
+ yield* sleep(`${sleepAfter} millis`);
1250
+ if (dozes.get(taskId) !== generation) return;
1251
+ dozes.delete(taskId);
1252
+ yield* perform(taskId, (yield* transact(taskId, existing({ type: "sleep" }))).effects);
1253
+ }));
1254
+ }
1255
+ function wake(taskId) {
1256
+ let found = wakeLocks.get(taskId);
1257
+ if (!found) wakeLocks.set(taskId, found = makeUnsafe$1(1));
1258
+ return found.withPermit(gen(function* () {
1259
+ dozes.set(taskId, (dozes.get(taskId) ?? 0) + 1);
1260
+ let { run } = yield* read(taskId);
1261
+ if (!run || !run.asleep || !executor) return run;
1262
+ if (executor.resume) yield* executor.resume(run).pipe(mapError((cause) => new WorkError({
1263
+ reason: cause.retryable ? "unavailable" : "refused",
1264
+ message: cause.message,
1265
+ cause
1266
+ })));
1267
+ let step = yield* transact(taskId, existing({ type: "wake" }));
1268
+ if (RUN_WAITING_STATES.includes(step.run.state)) yield* doze(taskId);
1269
+ return step.run;
1270
+ }));
1271
+ }
1272
+ function perform(taskId, effects) {
1273
+ return gen(function* () {
1274
+ for (let effect of effects) {
1275
+ let stored = (yield* read(taskId)).run;
1276
+ if (!stored || !executor) return;
1277
+ let run = stored;
1278
+ let awake = map(orElseSucceed(wake(taskId), () => run), (woken) => woken ?? run);
1279
+ switch (effect.type) {
1280
+ case "provision":
1281
+ yield* background(provision(taskId, run.id));
1282
+ break;
1283
+ case "send":
1284
+ run = yield* awake;
1285
+ yield* executor.send(run, effect.message).pipe(flatMap(() => follow(taskId, run)), catchTag("RunError", (error) => fail(taskId, error)));
1286
+ break;
1287
+ case "push":
1288
+ yield* background(push(taskId));
1289
+ break;
1290
+ case "stop":
1291
+ yield* ignore(executor.stop(run));
1292
+ break;
1293
+ case "destroy":
1294
+ yield* ignore(executor.destroy(run));
1295
+ break;
1296
+ case "doze":
1297
+ yield* doze(taskId);
1298
+ break;
1299
+ case "resume":
1300
+ if (effect.state !== "provisioning") run = yield* awake;
1301
+ if (effect.state === "provisioning") yield* background(provision(taskId, run.id));
1302
+ else if (effect.state === "pushing") yield* background(push(taskId));
1303
+ else if (effect.state === "working" || effect.state === "changes") yield* follow(taskId, run);
1304
+ }
1305
+ }
1306
+ });
1307
+ }
1308
+ function act(taskId, decide) {
1309
+ return gen(function* () {
1310
+ let step = yield* transact(taskId, decide);
1311
+ yield* perform(taskId, step.effects);
1312
+ return step.run;
1313
+ });
1314
+ }
1315
+ function existing(event) {
1316
+ return (run) => {
1317
+ if (!run) throw new RunTransitionError(null, event.type);
1318
+ return advanceRun(run, event, now());
1319
+ };
1320
+ }
1321
+ return {
1322
+ get: (taskId) => map(read(taskId), (result) => result.run),
1323
+ start: (taskId, by) => gen(function* () {
1324
+ if (!factory.sandbox) return yield* refused("Choose a sandbox in factory.sandbox before starting tasks.");
1325
+ if (!executor) return yield* refused(options.unavailable ?? `The sandbox ${factory.sandbox} isn’t available here.`);
1326
+ let { title } = yield* task(taskId);
1327
+ return yield* act(taskId, (previous) => startRun(previous, {
1328
+ id: newId(),
1329
+ taskId,
1330
+ title,
1331
+ branchPrefix: factory.branchPrefix,
1332
+ base: "main",
1333
+ provider: factory.sandbox,
1334
+ harness: factory.agent.harness,
1335
+ model: factory.agent.model,
1336
+ by
1337
+ }, now()));
1338
+ }),
1339
+ changes: (taskId, message, by) => act(taskId, existing({
1340
+ type: "changes",
1341
+ message,
1342
+ by
1343
+ })),
1344
+ answer: (taskId, message, by) => act(taskId, existing({
1345
+ type: "answer",
1346
+ message,
1347
+ by
1348
+ })),
1349
+ approve: (taskId, by) => act(taskId, existing({
1350
+ type: "approve",
1351
+ by
1352
+ })),
1353
+ discard: (taskId) => act(taskId, existing({ type: "discard" })),
1354
+ retry: (taskId) => act(taskId, existing({ type: "retry" })),
1355
+ wake,
1356
+ preview: (taskId) => gen(function* () {
1357
+ let { run } = yield* read(taskId);
1358
+ let preview = executor?.preview;
1359
+ if (!run || !preview || ![
1360
+ "working",
1361
+ "changes",
1362
+ "review",
1363
+ "needs_context",
1364
+ "pushing"
1365
+ ].includes(run.state)) return null;
1366
+ if (run.asleep) run = (yield* wake(taskId)) ?? run;
1367
+ return yield* preview(run).pipe(mapError((cause) => new WorkError({
1368
+ reason: "unavailable",
1369
+ message: cause.message,
1370
+ cause
1371
+ })));
1372
+ }),
1373
+ terminal: (taskId) => gen(function* () {
1374
+ let { run } = yield* read(taskId);
1375
+ let terminal = executor?.terminal;
1376
+ if (!run || !terminal || !run.sandbox || !runActive(run)) return null;
1377
+ if (run.asleep) run = (yield* wake(taskId)) ?? run;
1378
+ return yield* terminal(run).pipe(mapError((cause) => new WorkError({
1379
+ reason: "unavailable",
1380
+ message: cause.message,
1381
+ cause
1382
+ })));
1383
+ }),
1384
+ agentTask: (token) => gen(function* () {
1385
+ let claim = executor?.readToken?.(token);
1386
+ if (!claim) return null;
1387
+ let { run } = yield* read(claim.taskId);
1388
+ return run && run.id === claim.runId && runActive(run) ? claim.taskId : null;
1389
+ }),
1390
+ touch: (taskId) => gen(function* () {
1391
+ let { run } = yield* read(taskId);
1392
+ if (run && RUN_WAITING_STATES.includes(run.state) && !run.asleep && run.sandbox) yield* doze(taskId);
1393
+ }),
1394
+ checkMerged: (merged) => gen(function* () {
1395
+ let snapshot = yield* work.list(root, { limit: 1e3 });
1396
+ for (let item of snapshot.items) {
1397
+ if (item.run?.state !== "pushed") continue;
1398
+ let { run } = yield* read(item.id);
1399
+ if (run?.state !== "pushed" || !run.pushed || !(yield* merged(run.pushed))) continue;
1400
+ let step = yield* transact(item.id, existing({ type: "merged" })).pipe(catchIf((error) => error.reason === "refused", () => succeed(null)));
1401
+ if (!step) continue;
1402
+ yield* perform(item.id, step.effects);
1403
+ if (options.onMerged) yield* ignore(options.onMerged(step.run));
1404
+ }
1405
+ }),
1406
+ resume: gen(function* () {
1407
+ if (!executor) return;
1408
+ let current = yield* schema();
1409
+ let field = fieldByRole(current, "status");
1410
+ let candidates = (yield* work.list(root, { limit: 1e3 })).items.filter((item) => {
1411
+ let status = field ? item.values[field.id] : void 0;
1412
+ return [factory.statuses.inProgress, factory.statuses.needsApproval].includes(String(status));
1413
+ });
1414
+ for (let item of candidates) {
1415
+ let { run } = yield* read(item.id);
1416
+ if (!runActive(run) || underway.has(run.id)) continue;
1417
+ if (RUN_WAITING_STATES.includes(run.state) && !run.asleep && run.sandbox) {
1418
+ if (!dozes.has(item.id)) yield* doze(item.id);
1419
+ continue;
1420
+ }
1421
+ if (run.state === "provisioning") yield* background(report(item.id, {
1422
+ type: "fail",
1423
+ message: "Bud restarted while setting up the sandbox. Retry to set it up again.",
1424
+ retryable: true
1425
+ }));
1426
+ else if (run.state === "working" || run.state === "changes") yield* follow(item.id, run);
1427
+ else if (run.state === "pushing") yield* background(push(item.id));
1428
+ }
1429
+ })
1430
+ };
1431
+ });
1432
+ }
1433
+ //#endregion
1434
+ //#region ../factory/src/server/sandbox.ts
1435
+ /**
1436
+ * A clone URL Git can use with a token: SSH remotes become their HTTPS form.
1437
+ * @param url - The project's remote URL.
1438
+ * @returns The HTTPS URL, without credentials.
1439
+ */
1440
+ function httpsRemote(url) {
1441
+ let ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?\/?$/.exec(url.trim());
1442
+ if (ssh) return `https://${ssh[1]}/${ssh[2]}.git`;
1443
+ let parsed = new URL(url.trim());
1444
+ parsed.username = "";
1445
+ parsed.password = "";
1446
+ return parsed.href;
1447
+ }
1448
+ //#endregion
1449
+ //#region ../factory/src/server/sandbox-executor.ts
1450
+ var DEFAULT_TERMINAL_PORT = 4097;
1451
+ var DEFAULT_PREVIEW_PORT = 4098;
1452
+ var AGENT_START = 6e4;
1453
+ function agentGit(remote) {
1454
+ return {
1455
+ GIT_CONFIG_COUNT: "1",
1456
+ GIT_CONFIG_KEY_0: `remote.${remote}.pushurl`,
1457
+ GIT_CONFIG_VALUE_0: "bud-agents-do-not-push://"
1458
+ };
1459
+ }
1460
+ /**
1461
+ * The prompt a run starts with: the task, the project's instructions and the rules every run keeps.
1462
+ * @param task - The task.
1463
+ * @param run - The run, for its branch.
1464
+ * @param instructions - The project's instructions, if any.
1465
+ * @returns The prompt text.
1466
+ */
1467
+ function runPrompt(task, run, instructions) {
1468
+ return [
1469
+ `Finish this task in the project checked out here, on the branch ${run.branch}.`,
1470
+ "",
1471
+ `# ${task.title}`,
1472
+ "",
1473
+ task.description.trim() || "(No description.)",
1474
+ ...task.images?.length ? ["", `Attached: ${task.images.map((image) => image.name).join(", ")}, the page as the comment saw it, with its point marked.`] : [],
1475
+ ...instructions?.trim() ? [
1476
+ "",
1477
+ "# Project instructions",
1478
+ "",
1479
+ instructions.trim()
1480
+ ] : [],
1481
+ "",
1482
+ "# How to work",
1483
+ "",
1484
+ "- Stay on the current branch; commit your work with clear messages when it is done.",
1485
+ "- Run the project’s quick checks if there are any, and fix what you broke.",
1486
+ "- Never push, never change git remotes, credentials or the git configuration.",
1487
+ "- End with a short summary: what changed, and how a reviewer can see it.",
1488
+ `- If you can't finish without something only a person knows, such as a decision, a missing text or an account, don't guess: stop, and end your reply with a last line \`${NEEDS_CONTEXT_MARKER} <your question>\`. You'll get the answer in this session.`
1489
+ ].join("\n");
1490
+ }
1491
+ /**
1492
+ * Git options and environment that give one command a push token: a credential helper answering
1493
+ * from the command's own environment, so the token is never written anywhere or seen by the agent.
1494
+ * @param token - The token, or null to use Git's own credentials.
1495
+ * @param username - The user name the Git host expects with a token.
1496
+ * @returns The options to put after `git`, and the command's environment.
1497
+ */
1498
+ function pushCredential(token, username) {
1499
+ if (!token) return {
1500
+ auth: "",
1501
+ env: {}
1502
+ };
1503
+ return {
1504
+ auth: `-c credential.helper= -c 'credential.helper=!f() { test "$1" = get && printf "username=%s\\npassword=%s\\n" "$BUD_PUSH_USER" "$BUD_PUSH_TOKEN"; }; f' `,
1505
+ env: {
1506
+ BUD_PUSH_USER: username,
1507
+ BUD_PUSH_TOKEN: token
1508
+ }
1509
+ };
1510
+ }
1511
+ /**
1512
+ * Runs in sandboxes from a provider with a harness's agent, pushing the branch on approval.
1513
+ * @param options - The provider, the harness, the project, secrets and the factory config.
1514
+ * @returns The run executor, with the live preview of a run.
1515
+ */
1516
+ function sandboxExecutor(options) {
1517
+ let { sandbox, factory, harness } = options;
1518
+ let fail = (message, retryable = true, cause) => new RunError({
1519
+ message,
1520
+ retryable,
1521
+ cause
1522
+ });
1523
+ let derive = (purpose, run) => createHmac("sha256", options.secret).update(`${purpose}:${run.id}`).digest("hex").slice(0, 32);
1524
+ let password = (run) => derive("bud-agent", run);
1525
+ let terminalPassword = (run) => derive("bud-terminal", run);
1526
+ let promise = (operation, message) => tryPromise({
1527
+ try: operation,
1528
+ catch: (cause) => cause instanceof RunError ? cause : fail(`${message}: ${cause instanceof Error ? cause.message : String(cause)}`, true, cause)
1529
+ });
1530
+ let agent = (run) => gen(function* () {
1531
+ if (!run.sandbox || !run.ports.agent) return yield* fail("The run has no agent yet.", false);
1532
+ let port = yield* sandbox.expose(run.sandbox, run.ports.agent);
1533
+ let { url, headers } = port.server ?? {
1534
+ url: port.url,
1535
+ headers: {}
1536
+ };
1537
+ return harness.connect({
1538
+ url,
1539
+ headers,
1540
+ password: password(run)
1541
+ }, options.fetch);
1542
+ });
1543
+ let head = (id) => map(sandbox.exec(id, "git rev-parse HEAD"), (result) => result.exitCode === 0 ? result.output.trim() : null);
1544
+ let live = (client, session, checkout) => suspend$1(() => {
1545
+ let controller = new AbortController();
1546
+ return fromAsyncIterable(client.follow(session, () => runPromise(orElseSucceed(head(checkout), () => null)), controller.signal), (cause) => fail(`Lost the agent: ${cause instanceof Error ? cause.message : String(cause)}`, true, cause)).pipe(ensuring$1(sync(() => controller.abort())));
1547
+ });
1548
+ let port = (id, preferred) => sandbox.freePort ? sandbox.freePort(id) : succeed(preferred);
1549
+ return {
1550
+ provision: (run, task, created) => gen(function* () {
1551
+ let id = yield* sandbox.create({
1552
+ repository: options.repository,
1553
+ ref: run.base.branch,
1554
+ branch: run.branch,
1555
+ setup: factory.setup,
1556
+ env: {
1557
+ ...factory.dev?.env,
1558
+ ...options.env
1559
+ },
1560
+ secrets: options.secrets ?? [],
1561
+ labels: {
1562
+ run: run.id,
1563
+ task: task.id
1564
+ }
1565
+ });
1566
+ if (created) yield* created(id);
1567
+ let dev = null;
1568
+ if (factory.dev) {
1569
+ dev = yield* port(id, factory.dev.port);
1570
+ yield* sandbox.spawn(id, "dev", factory.dev.command.replaceAll("$PORT", String(dev)), { env: {
1571
+ PORT: String(dev),
1572
+ BUD_SANDBOX: "1",
1573
+ FORCE_COLOR: "1"
1574
+ } });
1575
+ }
1576
+ let preview = dev && options.page ? yield* gen(function* () {
1577
+ let proxy = yield* port(id, DEFAULT_PREVIEW_PORT);
1578
+ let directory = `"$(git rev-parse --absolute-git-dir)/bud-preview"`;
1579
+ let file = (name, content) => `cat > ${directory}/${name} <<'BUD_FILE_END'\n${content}\nBUD_FILE_END`;
1580
+ if ((yield* sandbox.exec(id, [
1581
+ `mkdir -p ${directory}`,
1582
+ file("proxy.py", "import http.client, os, socket, sys, threading\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\n\nUPSTREAM = int(sys.argv[2])\nFILES = os.environ.get('BUD_PREVIEW_FILES', '')\nPICKER = os.environ.get('BUD_PICKER_PATH', '')\nHOP = {'connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade'}\n\n\ndef read(name):\n try:\n with open(os.path.join(FILES, name), 'rb') as file:\n return file.read()\n except OSError:\n return b''\n\n\nclass Handler(BaseHTTPRequestHandler):\n protocol_version = 'HTTP/1.1'\n\n def log_message(self, *args):\n pass\n\n def headers_for_upstream(self):\n headers = {}\n for key, value in self.headers.items():\n # The page's own address stays, as the dev server would see it without the proxy.\n if key.lower() in HOP or key.lower() == 'accept-encoding':\n continue\n headers[key] = value\n return headers\n\n def tunnel(self):\n upstream = socket.create_connection(('127.0.0.1', UPSTREAM))\n lines = ['%s %s HTTP/1.1' % (self.command, self.path)]\n for key, value in self.headers.items():\n lines.append('%s: %s' % (key, value))\n upstream.sendall(('\\r\\n'.join(lines) + '\\r\\n\\r\\n').encode('latin-1'))\n client = self.connection\n\n def pipe(source, target):\n try:\n while True:\n data = source.recv(65536)\n if not data:\n break\n target.sendall(data)\n except OSError:\n pass\n finally:\n for end in (source, target):\n try:\n end.shutdown(socket.SHUT_RDWR)\n except OSError:\n pass\n\n threading.Thread(target=pipe, args=(upstream, client), daemon=True).start()\n pipe(client, upstream)\n self.close_connection = True\n\n def forward(self):\n if self.headers.get('Upgrade'):\n return self.tunnel()\n length = int(self.headers.get('Content-Length') or 0)\n body = self.rfile.read(length) if length else None\n connection = http.client.HTTPConnection('127.0.0.1', UPSTREAM, timeout=300)\n try:\n connection.request(self.command, self.path, body=body, headers=self.headers_for_upstream())\n response = connection.getresponse()\n except OSError:\n self.send_response(502)\n self.send_header('Content-Length', '0')\n self.end_headers()\n return\n path = self.path.split('?')[0]\n # The picker Bud's page bridge imports, when the project serves none of its own; a dev\n # server may answer an unknown path with its page instead of a 404.\n script = 'javascript' in (response.getheader('Content-Type') or '')\n if PICKER and path == PICKER and (response.status != 200 or not script):\n response.read()\n data = read('picker.js')\n self.send_response(200)\n self.send_header('Content-Type', 'text/javascript')\n self.send_header('Content-Length', str(len(data)))\n self.end_headers()\n self.wfile.write(data)\n return\n html = (response.getheader('Content-Type') or '').startswith('text/html')\n self.send_response(response.status, response.reason)\n for key, value in response.getheaders():\n if key.lower() in HOP or key.lower() in ('content-length', 'server', 'date'):\n continue\n # A redirect to the dev server's own address stays on the page's.\n if key.lower() == 'location':\n for local in ('http://127.0.0.1:%d' % UPSTREAM, 'http://localhost:%d' % UPSTREAM):\n if value.startswith(local):\n value = value[len(local):] or '/'\n self.send_header(key, value)\n if html or self.command == 'HEAD':\n data = response.read()\n if html:\n script = b'<script type=\"module\">' + read('bridge.js') + b'<\/script>'\n head = data.lower().find(b'<head>')\n data = data[: head + 6] + script + data[head + 6 :] if head >= 0 else script + data\n self.send_header('Content-Length', str(len(data)))\n self.end_headers()\n if self.command != 'HEAD':\n self.wfile.write(data)\n return\n if response.status in (204, 304) or response.status < 200:\n self.send_header('Content-Length', '0')\n self.end_headers()\n return\n self.send_header('Transfer-Encoding', 'chunked')\n self.end_headers()\n try:\n while True:\n chunk = response.read1(65536) if hasattr(response, 'read1') else response.read(65536)\n if not chunk:\n break\n self.wfile.write(b'%x\\r\\n%s\\r\\n' % (len(chunk), chunk))\n self.wfile.flush()\n self.wfile.write(b'0\\r\\n\\r\\n')\n except OSError:\n self.close_connection = True\n\n do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = do_OPTIONS = do_HEAD = forward\n\n\nif __name__ == '__main__':\n ThreadingHTTPServer.daemon_threads = True\n ThreadingHTTPServer(('127.0.0.1', int(sys.argv[1])), Handler).serve_forever()\n"),
1583
+ file("bridge.js", options.page.bridge),
1584
+ file("picker.js", options.page.picker)
1585
+ ].join("\n"))).exitCode !== 0) return null;
1586
+ yield* sandbox.spawn(id, "preview", `BUD_PREVIEW_FILES=${directory} exec python3 ${directory}/proxy.py ${proxy} ${dev}`, { env: { BUD_PICKER_PATH: options.page.pickerPath } });
1587
+ return proxy;
1588
+ }).pipe(orElseSucceed(() => null)) : null;
1589
+ let agentPort = yield* port(id, harness.port);
1590
+ let started = {
1591
+ ...run,
1592
+ sandbox: id,
1593
+ ports: {
1594
+ dev,
1595
+ agent: agentPort,
1596
+ ...preview ? { preview } : {}
1597
+ }
1598
+ };
1599
+ let tools = options.tools?.();
1600
+ let server = harness.start({
1601
+ port: agentPort,
1602
+ password: password(started),
1603
+ ...tools ? { tools: {
1604
+ url: tools,
1605
+ token: agentToken(options.secret, {
1606
+ id: run.id,
1607
+ taskId: task.id
1608
+ })
1609
+ } } : {}
1610
+ });
1611
+ yield* sandbox.spawn(id, "agent", server.command, { env: {
1612
+ ...server.env,
1613
+ ...agentGit(factory.push.remote)
1614
+ } });
1615
+ let terminal = yield* gen(function* () {
1616
+ let bridgePort = yield* port(id, DEFAULT_TERMINAL_PORT);
1617
+ let file = `"$(git rev-parse --absolute-git-dir)/bud-terminal.py"`;
1618
+ if ((yield* sandbox.exec(id, `cat > ${file} <<'BUD_TERMINAL'\nimport base64, fcntl, json, os, pty, select, signal, struct, sys, termios, threading
1619
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
1620
+
1621
+ PASSWORD = os.environ.pop('BUD_TERMINAL_PASSWORD', '')
1622
+ LIMIT = 512 * 1024
1623
+ sessions = {}
1624
+ lock = threading.Lock()
1625
+
1626
+
1627
+ def clamp(value):
1628
+ return max(2, min(int(value), 1000))
1629
+
1630
+
1631
+ class Session:
1632
+ def __init__(self, command, cols, rows, env):
1633
+ self.buffer = bytearray()
1634
+ self.start = 0
1635
+ self.done = False
1636
+ self.changed = threading.Condition()
1637
+ pid, fd = pty.fork()
1638
+ if pid == 0:
1639
+ # The size is set before the command starts, so it never reads an empty one.
1640
+ fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack('HHHH', clamp(rows), clamp(cols), 0, 0))
1641
+ os.environ.update(env)
1642
+ os.environ['TERM'] = 'xterm-256color'
1643
+ os.environ['COLORTERM'] = 'truecolor'
1644
+ os.execvp('sh', ['sh', '-c', command])
1645
+ self.pid = pid
1646
+ self.fd = fd
1647
+ self.resize(cols, rows)
1648
+ threading.Thread(target=self.read, daemon=True).start()
1649
+
1650
+ def read(self):
1651
+ while True:
1652
+ try:
1653
+ ready, _, _ = select.select([self.fd], [], [], 1)
1654
+ if not ready:
1655
+ continue
1656
+ data = os.read(self.fd, 65536)
1657
+ except OSError:
1658
+ data = b''
1659
+ with self.changed:
1660
+ if not data:
1661
+ self.done = True
1662
+ self.changed.notify_all()
1663
+ return
1664
+ self.buffer += data
1665
+ if len(self.buffer) > LIMIT:
1666
+ drop = len(self.buffer) - LIMIT
1667
+ del self.buffer[:drop]
1668
+ self.start += drop
1669
+ self.changed.notify_all()
1670
+
1671
+ def resize(self, cols, rows):
1672
+ fcntl.ioctl(self.fd, termios.TIOCSWINSZ, struct.pack('HHHH', clamp(rows), clamp(cols), 0, 0))
1673
+
1674
+ def write(self, data):
1675
+ view = memoryview(data)
1676
+ while view:
1677
+ written = os.write(self.fd, view)
1678
+ view = view[written:]
1679
+
1680
+ def close(self):
1681
+ try:
1682
+ os.killpg(os.getpgid(self.pid), signal.SIGHUP)
1683
+ except OSError:
1684
+ pass
1685
+
1686
+
1687
+ class Handler(BaseHTTPRequestHandler):
1688
+ protocol_version = 'HTTP/1.1'
1689
+
1690
+ def log_message(self, *args):
1691
+ pass
1692
+
1693
+ def authorized(self):
1694
+ expected = 'Basic ' + base64.b64encode(('bud:' + PASSWORD).encode()).decode()
1695
+ if PASSWORD and self.headers.get('Authorization') == expected:
1696
+ return True
1697
+ self.send_response(401)
1698
+ self.send_header('Content-Length', '0')
1699
+ self.end_headers()
1700
+ return False
1701
+
1702
+ def reply(self, status, body=None):
1703
+ data = json.dumps(body).encode() if body is not None else b''
1704
+ self.send_response(status)
1705
+ self.send_header('Content-Type', 'application/json')
1706
+ self.send_header('Content-Length', str(len(data)))
1707
+ self.end_headers()
1708
+ self.wfile.write(data)
1709
+
1710
+ def body(self):
1711
+ length = int(self.headers.get('Content-Length') or 0)
1712
+ return self.rfile.read(length) if length else b''
1713
+
1714
+ def session(self, query):
1715
+ name = query.get('id', '')
1716
+ with lock:
1717
+ return sessions.get(name)
1718
+
1719
+ def query(self):
1720
+ from urllib.parse import parse_qs, urlparse
1721
+ url = urlparse(self.path)
1722
+ return url.path, {key: values[0] for key, values in parse_qs(url.query).items()}
1723
+
1724
+ def do_GET(self):
1725
+ if not self.authorized():
1726
+ return
1727
+ path, query = self.query()
1728
+ if path == '/health':
1729
+ return self.reply(200, {'ok': True})
1730
+ if path != '/stream':
1731
+ return self.reply(404)
1732
+ session = self.session(query)
1733
+ if not session:
1734
+ return self.reply(404)
1735
+ offset = int(query.get('from') or 0)
1736
+ self.send_response(200)
1737
+ self.send_header('Content-Type', 'text/event-stream')
1738
+ self.send_header('Cache-Control', 'no-store')
1739
+ self.send_header('Transfer-Encoding', 'chunked')
1740
+ self.end_headers()
1741
+
1742
+ def send(text):
1743
+ data = text.encode()
1744
+ self.wfile.write(b'%x\\r\\n%s\\r\\n' % (len(data), data))
1745
+ self.wfile.flush()
1746
+
1747
+ try:
1748
+ send(': connected\\n\\n')
1749
+ while True:
1750
+ with session.changed:
1751
+ end = session.start + len(session.buffer)
1752
+ while offset >= end and not session.done:
1753
+ if not session.changed.wait(15):
1754
+ break
1755
+ end = session.start + len(session.buffer)
1756
+ begin = max(offset, session.start)
1757
+ chunk = bytes(session.buffer[begin - session.start:end - session.start])
1758
+ done = session.done and begin >= end
1759
+ if chunk:
1760
+ send('id: %d\\ndata: %s\\n\\n' % (end, base64.b64encode(chunk).decode()))
1761
+ offset = end
1762
+ elif done:
1763
+ send('event: exit\\ndata: {}\\n\\n')
1764
+ break
1765
+ else:
1766
+ send(': keepalive\\n\\n')
1767
+ self.wfile.write(b'0\\r\\n\\r\\n')
1768
+ except (BrokenPipeError, ConnectionResetError):
1769
+ pass
1770
+
1771
+ def do_POST(self):
1772
+ if not self.authorized():
1773
+ return
1774
+ path, query = self.query()
1775
+ body = self.body()
1776
+ if path == '/open':
1777
+ spec = json.loads(body or b'{}')
1778
+ name = str(spec.get('id', ''))
1779
+ with lock:
1780
+ existing = sessions.get(name)
1781
+ if existing and not existing.done:
1782
+ return self.reply(200, {'opened': False})
1783
+ env = spec.get('env') or {}
1784
+ sessions[name] = Session(
1785
+ str(spec['command']),
1786
+ spec.get('cols', 80),
1787
+ spec.get('rows', 24),
1788
+ {str(key): str(value) for key, value in env.items()},
1789
+ )
1790
+ return self.reply(200, {'opened': True})
1791
+ session = self.session(query)
1792
+ if not session:
1793
+ return self.reply(404)
1794
+ if path == '/input':
1795
+ session.write(body)
1796
+ return self.reply(204)
1797
+ if path == '/resize':
1798
+ size = json.loads(body or b'{}')
1799
+ session.resize(size.get('cols', 80), size.get('rows', 24))
1800
+ return self.reply(204)
1801
+ if path == '/close':
1802
+ session.close()
1803
+ return self.reply(204)
1804
+ return self.reply(404)
1805
+
1806
+
1807
+ if __name__ == '__main__':
1808
+ ThreadingHTTPServer.daemon_threads = True
1809
+ ThreadingHTTPServer(('127.0.0.1', int(sys.argv[1])), Handler).serve_forever()
1810
+ \nBUD_TERMINAL`)).exitCode !== 0) return null;
1811
+ yield* sandbox.spawn(id, "terminal", `exec python3 ${file} ${bridgePort}`, { env: { BUD_TERMINAL_PASSWORD: terminalPassword(started) } });
1812
+ return bridgePort;
1813
+ }).pipe(orElseSucceed(() => null));
1814
+ started = {
1815
+ ...started,
1816
+ ports: {
1817
+ ...started.ports,
1818
+ ...terminal ? { terminal } : {}
1819
+ }
1820
+ };
1821
+ let client = yield* agent(started);
1822
+ let instructions = options.instructions ? yield* options.instructions : null;
1823
+ return {
1824
+ sandbox: id,
1825
+ session: yield* promise(async () => {
1826
+ let until = Date.now() + AGENT_START;
1827
+ while (!await client.healthy()) {
1828
+ if (Date.now() > until) throw fail("The agent didn’t start within a minute.");
1829
+ await new Promise((resolve) => setTimeout(resolve, 500));
1830
+ }
1831
+ let session = await client.session(task.title);
1832
+ await client.prompt(session, runPrompt(task, run, instructions), task.images);
1833
+ return session;
1834
+ }, "Could not start the agent"),
1835
+ commit: yield* head(id),
1836
+ ports: started.ports
1837
+ };
1838
+ }),
1839
+ follow: (run) => unwrap$1(gen(function* () {
1840
+ let client = yield* agent(run);
1841
+ let session = run.session;
1842
+ if (!session) return fail$2(fail("The run has no agent session.", false));
1843
+ let checkout = run.sandbox;
1844
+ return live(client, session, checkout);
1845
+ })),
1846
+ send: (run, message) => gen(function* () {
1847
+ let client = yield* agent(run);
1848
+ if (!run.session) return yield* fail("The run has no agent session.", false);
1849
+ let session = run.session;
1850
+ yield* promise(() => client.prompt(session, `${message}\n\nKeep working on the same branch, commit, and end with a short summary.`), "Could not reach the agent");
1851
+ }),
1852
+ push: (run) => gen(function* () {
1853
+ if (!run.sandbox) return yield* fail("The run has no sandbox.", false);
1854
+ let id = run.sandbox;
1855
+ let commit = yield* head(id);
1856
+ if (!commit) return yield* fail("The branch has no commit to push.", false);
1857
+ let { remote } = factory.push;
1858
+ let ref = `refs/heads/${run.branch}`;
1859
+ let token = options.pushToken ?? null;
1860
+ let { auth, env } = pushCredential(token, factory.push.username);
1861
+ let hide = (text) => token ? text.split(token).join("•••") : text;
1862
+ let git = (command) => sandbox.exec(id, `git ${auth}${command}`, {
1863
+ env,
1864
+ timeoutMs: 12e4
1865
+ });
1866
+ let remoteHead = () => map(git(`ls-remote '${remote}' '${ref}'`), (result) => result.exitCode === 0 ? result.output.trim().split(/\s+/)[0] ?? "" : null);
1867
+ if ((yield* remoteHead()) === commit) return commit;
1868
+ let pushed = yield* git(`push '${remote}' 'HEAD:${ref}'`);
1869
+ if (pushed.exitCode === 0) return commit;
1870
+ if ((yield* remoteHead()) === commit) return commit;
1871
+ return yield* fail(`The push was refused: ${hide(pushed.output.trim().slice(-400))}`, true);
1872
+ }),
1873
+ stop: (run) => run.sandbox ? sandbox.stop(run.sandbox) : void_$1,
1874
+ resume: (run) => gen(function* () {
1875
+ if (!run.sandbox) return yield* fail("The run has no sandbox.", false);
1876
+ yield* sandbox.resume(run.sandbox);
1877
+ let client = yield* agent(run);
1878
+ yield* promise(async () => {
1879
+ let until = Date.now() + AGENT_START;
1880
+ while (!await client.healthy()) {
1881
+ if (Date.now() > until) throw fail("The agent didn’t start again within a minute.");
1882
+ await new Promise((resolve) => setTimeout(resolve, 500));
1883
+ }
1884
+ }, "Could not wake the agent");
1885
+ }),
1886
+ destroy: (run) => run.sandbox ? sandbox.destroy(run.sandbox) : void_$1,
1887
+ readToken: (token) => readAgentToken(options.secret, token),
1888
+ terminal: (run) => gen(function* () {
1889
+ if (!run.sandbox || !run.ports.terminal) return null;
1890
+ let id = run.sandbox;
1891
+ let port = yield* sandbox.expose(id, run.ports.terminal);
1892
+ let { url, headers } = port.server ?? {
1893
+ url: port.url,
1894
+ headers: {}
1895
+ };
1896
+ let attach = run.session && run.ports.agent && harness.attach ? harness.attach({
1897
+ port: run.ports.agent,
1898
+ password: password(run),
1899
+ session: run.session
1900
+ }) : null;
1901
+ let log = factory.dev ? sandbox.logFile?.(id, "dev") : void 0;
1902
+ return {
1903
+ url,
1904
+ headers,
1905
+ password: terminalPassword(run),
1906
+ sessions: {
1907
+ agent: attach,
1908
+ dev: log ? {
1909
+ command: `exec tail -n 400 -F "${log}"`,
1910
+ env: {}
1911
+ } : null,
1912
+ shell: {
1913
+ command: "exec \"${SHELL:-/bin/sh}\" -i",
1914
+ env: {}
1915
+ }
1916
+ }
1917
+ };
1918
+ }),
1919
+ preview: (run) => run.sandbox && run.ports.dev ? map(sandbox.expose(run.sandbox, run.ports.preview ?? run.ports.dev, { public: true }), ({ url }) => {
1920
+ let page = new URL(url);
1921
+ page.pathname = factory.dev?.path ?? "/";
1922
+ return { url: page.href };
1923
+ }) : succeed(null)
1924
+ };
1925
+ }
1926
+ //#endregion
1927
+ //#region ../harness-opencode/src/opencode.server.ts
1928
+ /**
1929
+ * Splits a configured model into OpenCode's provider and model.
1930
+ * @param model - Such as `deepseek/deepseek-v4.1-flash`.
1931
+ * @returns The provider and model ids.
1932
+ */
1933
+ function opencodeModel(model) {
1934
+ let slash = model.indexOf("/");
1935
+ if (slash < 1 || slash === model.length - 1) throw new TypeError(`The model ${model} must be provider/model.`);
1936
+ return {
1937
+ providerID: model.slice(0, slash),
1938
+ modelID: model.slice(slash + 1)
1939
+ };
1940
+ }
1941
+ /**
1942
+ * OpenCode's config for an agent working in a sandbox: it may edit and run commands without asking,
1943
+ * since nothing leaves the sandbox until a person approves.
1944
+ * @param model - The configured model.
1945
+ * @param tools - Bud's task database as an MCP server, when the sandbox reaches it.
1946
+ * @returns The config as `OPENCODE_CONFIG_CONTENT` takes it.
1947
+ */
1948
+ function opencodeConfig(model, tools) {
1949
+ return JSON.stringify({
1950
+ $schema: "https://opencode.ai/config.json",
1951
+ model,
1952
+ permission: {
1953
+ edit: "allow",
1954
+ bash: "allow",
1955
+ webfetch: "allow"
1956
+ },
1957
+ autoupdate: false,
1958
+ share: "disabled",
1959
+ ...tools ? { mcp: { bud: {
1960
+ type: "remote",
1961
+ url: tools.url,
1962
+ headers: { Authorization: `Bearer ${tools.token}` },
1963
+ enabled: true
1964
+ } } } : {}
1965
+ });
1966
+ }
1967
+ /**
1968
+ * A client for one `opencode serve`.
1969
+ * @param options - Its URL and password.
1970
+ * @returns Health, sessions, prompts and its event stream.
1971
+ */
1972
+ function opencodeClient(options) {
1973
+ let request = options.fetch ?? fetch;
1974
+ let base = options.url.replace(/\/$/, "");
1975
+ let headers = {
1976
+ ...options.headers,
1977
+ Authorization: `Basic ${btoa(`opencode:${options.password}`)}`,
1978
+ "Content-Type": "application/json"
1979
+ };
1980
+ let address = (path) => {
1981
+ let url = new URL(base);
1982
+ url.pathname = `${url.pathname.replace(/\/$/, "")}${path}`;
1983
+ return url.href;
1984
+ };
1985
+ async function call(path, init = {}) {
1986
+ let response = await request(address(path), {
1987
+ ...init,
1988
+ headers: {
1989
+ ...headers,
1990
+ ...init.headers
1991
+ }
1992
+ });
1993
+ if (!response.ok) throw new Error(`OpenCode answered ${response.status} for ${path}: ${(await response.text()).slice(0, 300)}`);
1994
+ return response;
1995
+ }
1996
+ return {
1997
+ async healthy() {
1998
+ return request(address("/global/health"), { headers }).then((response) => response.ok, () => false);
1999
+ },
2000
+ async session(title) {
2001
+ let session = await (await call("/session", {
2002
+ method: "POST",
2003
+ body: JSON.stringify({ title })
2004
+ })).json();
2005
+ if (typeof session.id !== "string") throw new Error("OpenCode returned no session.");
2006
+ return session.id;
2007
+ },
2008
+ async prompt(session, model, text, files = []) {
2009
+ let path = `/session/${encodeURIComponent(session)}`;
2010
+ let count = async () => {
2011
+ let messages = await (await call(`${path}/message`)).json();
2012
+ return Array.isArray(messages) ? messages.length : 0;
2013
+ };
2014
+ let before = await count();
2015
+ await call(`${path}/prompt_async`, {
2016
+ method: "POST",
2017
+ body: JSON.stringify({
2018
+ model,
2019
+ parts: [{
2020
+ type: "text",
2021
+ text
2022
+ }, ...files.map((file) => ({
2023
+ type: "file",
2024
+ mime: file.mime,
2025
+ filename: file.name,
2026
+ url: `data:${file.mime};base64,${file.data}`
2027
+ }))]
2028
+ })
2029
+ });
2030
+ for (let tries = 0; tries < 60; tries++) {
2031
+ if (await count() > before) return;
2032
+ await new Promise((resolve) => setTimeout(resolve, 250));
2033
+ }
2034
+ },
2035
+ async turn(session) {
2036
+ if (record$1(record$1(await (await call("/session/status")).json())[session]).type === "busy") return { state: "working" };
2037
+ let messages = await (await call(`/session/${encodeURIComponent(session)}/message`)).json();
2038
+ let list = Array.isArray(messages) ? messages.map((message) => record$1(message)) : [];
2039
+ let info = record$1(list.at(-1)?.info);
2040
+ if (info.role === "user" || info.role === "assistant" && !record$1(info.time).completed) return { state: "working" };
2041
+ let error = record$1(info.error);
2042
+ if (Object.keys(error).length) {
2043
+ let data = record$1(error.data);
2044
+ return {
2045
+ state: "failed",
2046
+ message: typeof data.message === "string" ? data.message : String(error.name ?? ""),
2047
+ retryable: data.isRetryable === true
2048
+ };
2049
+ }
2050
+ for (let message of list.toReversed()) {
2051
+ if (record$1(message.info).role !== "assistant") continue;
2052
+ let text = replyText(message.parts);
2053
+ if (text) return {
2054
+ state: "done",
2055
+ summary: text
2056
+ };
2057
+ }
2058
+ return {
2059
+ state: "done",
2060
+ summary: ""
2061
+ };
2062
+ },
2063
+ async *events(signal) {
2064
+ let response = await call("/event", {
2065
+ headers: { Accept: "text/event-stream" },
2066
+ signal
2067
+ });
2068
+ yield {
2069
+ type: "bud.subscribed",
2070
+ properties: {}
2071
+ };
2072
+ let reader = response.body.getReader();
2073
+ let decoder = new TextDecoder();
2074
+ let buffer = "";
2075
+ try {
2076
+ while (true) {
2077
+ let { done, value } = await reader.read();
2078
+ if (done) return;
2079
+ buffer += decoder.decode(value, { stream: true });
2080
+ let blocks = buffer.split("\n\n");
2081
+ buffer = blocks.pop() ?? "";
2082
+ for (let block of blocks) {
2083
+ let data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2084
+ if (!data) continue;
2085
+ try {
2086
+ let event = JSON.parse(data);
2087
+ if (event && typeof event.type === "string") yield event;
2088
+ } catch {}
2089
+ }
2090
+ }
2091
+ } finally {
2092
+ await reader.cancel().catch(() => {});
2093
+ }
2094
+ }
2095
+ };
2096
+ }
2097
+ function replyText(parts) {
2098
+ return Array.isArray(parts) ? parts.map((part) => record$1(part)).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim() : "";
2099
+ }
2100
+ /**
2101
+ * Reads an untrusted JSON value as an object.
2102
+ * @param value - Anything.
2103
+ * @returns The object, or an empty one.
2104
+ */
2105
+ function record$1(value) {
2106
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2107
+ }
2108
+ /**
2109
+ * Turns OpenCode's events into a run's: completed tool calls as activity, the agent's last reply as
2110
+ * its summary when the session is idle, and session errors as a failure.
2111
+ * @param events - OpenCode's event stream.
2112
+ * @param session - The run's session; other sessions' events are skipped.
2113
+ * @param head - Reads the checkout's commit once the agent is done.
2114
+ * @param turn - Reads the session's last turn once subscribed, for a turn that ended before.
2115
+ * @returns The run's events, ending with `idle` or `fail`.
2116
+ */
2117
+ async function* followSession(events, session, head, turn) {
2118
+ let assistant = /* @__PURE__ */ new Set();
2119
+ let replies = /* @__PURE__ */ new Map();
2120
+ let order = [];
2121
+ let busy = Boolean(turn);
2122
+ let idle = async (summary) => ({
2123
+ type: "idle",
2124
+ summary: summary.trim() || "The agent finished without a summary.",
2125
+ head: await head()
2126
+ });
2127
+ for await (let event of events) {
2128
+ if (event.type === "bud.subscribed") {
2129
+ let last = await turn?.();
2130
+ if (last?.state === "done") return yield await idle(last.summary);
2131
+ if (last?.state === "failed") return yield {
2132
+ type: "fail",
2133
+ message: `The agent failed: ${last.message}`,
2134
+ retryable: last.retryable
2135
+ };
2136
+ continue;
2137
+ }
2138
+ let properties = record$1(event.properties);
2139
+ if (properties.sessionID !== session) continue;
2140
+ if (event.type === "message.updated") {
2141
+ let info = record$1(properties.info);
2142
+ if (info.role === "assistant" && typeof info.id === "string") assistant.add(info.id);
2143
+ } else if (event.type === "message.part.updated") {
2144
+ let part = record$1(properties.part);
2145
+ let state = record$1(part.state);
2146
+ if (part.type === "tool" && state.status === "completed") yield {
2147
+ type: "activity",
2148
+ entry: {
2149
+ kind: "tool",
2150
+ text: `${String(part.tool)}${typeof state.title === "string" && state.title ? `: ${state.title}` : ""}`
2151
+ }
2152
+ };
2153
+ else if (part.type === "text" && typeof part.text === "string" && typeof part.messageID === "string") {
2154
+ if (!replies.has(part.messageID)) order.push(part.messageID);
2155
+ replies.set(part.messageID, part.text);
2156
+ }
2157
+ } else if (event.type === "session.status") busy = record$1(properties.status).type === "busy" || busy;
2158
+ else if (event.type === "session.error") {
2159
+ let error = record$1(properties.error);
2160
+ let data = record$1(error.data);
2161
+ yield {
2162
+ type: "fail",
2163
+ message: `The agent failed: ${typeof data.message === "string" ? data.message : String(error.name ?? "The agent failed.")}`,
2164
+ retryable: data.isRetryable === true
2165
+ };
2166
+ return;
2167
+ } else if (event.type === "session.idle" && busy) {
2168
+ let last = order.findLast((id) => assistant.has(id));
2169
+ yield await idle(last ? replies.get(last) ?? "" : "");
2170
+ return;
2171
+ }
2172
+ }
2173
+ }
2174
+ /**
2175
+ * OpenCode as the factory's agent: `opencode serve` in the sandbox, with a password Bud derives
2176
+ * per run, and OpenCode's events followed to the agent's summary.
2177
+ * @param options - The model as OpenCode names it, `provider/model`, and the server variables
2178
+ * its key is in.
2179
+ * @returns The harness.
2180
+ */
2181
+ function opencodeHarness(options) {
2182
+ let model = opencodeModel(options.model);
2183
+ return {
2184
+ id: "opencode",
2185
+ model: options.model,
2186
+ keys: options.keys ?? [],
2187
+ port: 4096,
2188
+ start: ({ port, password, tools }) => ({
2189
+ command: `opencode serve --port ${port} --hostname 127.0.0.1`,
2190
+ env: {
2191
+ OPENCODE_SERVER_PASSWORD: password,
2192
+ OPENCODE_CONFIG_CONTENT: opencodeConfig(options.model, tools)
2193
+ }
2194
+ }),
2195
+ attach: ({ port, password, session }) => ({
2196
+ command: `opencode attach http://127.0.0.1:${port} --session '${session.replaceAll("'", "")}'`,
2197
+ env: { OPENCODE_SERVER_PASSWORD: password }
2198
+ }),
2199
+ connect: (address, request) => {
2200
+ let client = opencodeClient({
2201
+ ...address,
2202
+ fetch: request
2203
+ });
2204
+ return {
2205
+ healthy: () => client.healthy(),
2206
+ session: (title) => client.session(title),
2207
+ prompt: (session, text, images) => client.prompt(session, model, text, images),
2208
+ follow: (session, head, signal) => followSession(client.events(signal), session, head, () => client.turn(session))
2209
+ };
2210
+ }
2211
+ };
2212
+ }
2213
+ //#endregion
2214
+ //#region app/server/factory-runs.ts
2215
+ /** The project's factory runs: what Start task, Approve and the rest do. */
2216
+ var FactoryRuns = class extends Service()("@bud.sh/app/FactoryRuns") {};
2217
+ succeed$1(RunExecutor, null);
2218
+ var unavailable = /* @__PURE__ */ new WeakMap();
2219
+ /**
2220
+ * What the preview proxy in each sandbox adds to the project's pages: Bud's page bridge, and its
2221
+ * DOM picker where the project serves none, so Agent view follows and picks in any project.
2222
+ */
2223
+ var previewPage = {
2224
+ bridge: bridgeScript,
2225
+ picker: pickerModule("bud:dom", null),
2226
+ pickerPath: PICKER_MODULE_URL
2227
+ };
2228
+ var origins = /* @__PURE__ */ new WeakMap();
2229
+ /**
2230
+ * Remembers where this Bud listens, so agents in sandboxes on this machine reach its task
2231
+ * database.
2232
+ * @param project - The connected project.
2233
+ * @param origin - Such as `http://127.0.0.1:4321`.
2234
+ */
2235
+ function rememberOrigin(project, origin) {
2236
+ origins.set(project, origin);
2237
+ }
2238
+ function agentSecret(root) {
2239
+ return tryPromise({
2240
+ try: async () => {
2241
+ let directory = join(root, "node_modules/.cache/bud");
2242
+ let file = join(directory, "factory-secret");
2243
+ let existing = (await readFile(file, "utf8").catch(() => "")).trim();
2244
+ if (/^[0-9a-f]{64}$/.test(existing)) return existing;
2245
+ let secret = randomBytes(32).toString("hex");
2246
+ await mkdir(directory, { recursive: true });
2247
+ await writeFile(file, `${secret}\n`, { mode: 384 });
2248
+ return secret;
2249
+ },
2250
+ catch: (cause) => new AdapterStartupError({
2251
+ message: "Could not keep the factory’s agent secret.",
2252
+ cause
2253
+ })
2254
+ });
2255
+ }
2256
+ /**
2257
+ * Carries out runs in the sandboxes the project's sandbox plugin makes, with its harness's agent.
2258
+ * @param project - The connected project and its factory config.
2259
+ * @returns The executor; null when the factory or its sandbox is off, or can't run here.
2260
+ */
2261
+ function runExecutorLayer(project) {
2262
+ return effect(RunExecutor, gen(function* () {
2263
+ let factory = project.config.factory;
2264
+ let selected = factory?.sandbox;
2265
+ if (!factory || !selected) return null;
2266
+ let credential = factory.push.credential;
2267
+ let pushToken = credential ? process.env[credential] ?? null : null;
2268
+ let keys = Object.fromEntries(factory.agent.keys.flatMap((name) => {
2269
+ let value = process.env[name];
2270
+ return value ? [[name, value]] : [];
2271
+ }));
2272
+ let plugins = configPlugins(project.config);
2273
+ let plugin = plugins.find((candidate) => candidate.name === selected);
2274
+ if (!plugin?.sandbox) return null;
2275
+ let harness = plugins.find((candidate) => candidate.name === factory.agent.harness)?.harness ?? opencodeHarness(factory.agent);
2276
+ let remote = yield* gen(function* () {
2277
+ return yield* (yield* (yield* Git).discover(project.root)).remoteUrl(factory.push.remote);
2278
+ }).pipe(orElseSucceed(() => null));
2279
+ let https = remote ? yield* try_(() => httpsRemote(remote)).pipe(orElseSucceed(() => null)) : null;
2280
+ let sandbox = yield* try_(() => {
2281
+ let made = plugin.sandbox({
2282
+ root: project.root,
2283
+ env: process.env,
2284
+ repository: https,
2285
+ clone: pushToken ? {
2286
+ username: factory.push.username,
2287
+ token: pushToken
2288
+ } : null,
2289
+ withhold: credential ? [credential] : []
2290
+ });
2291
+ if (!made || typeof made !== "object" || typeof made.id !== "string" || [
2292
+ "create",
2293
+ "status",
2294
+ "exec",
2295
+ "spawn",
2296
+ "expose",
2297
+ "stop",
2298
+ "resume",
2299
+ "destroy"
2300
+ ].some((name) => typeof made[name] !== "function")) throw new Error(`The plugin ${plugin.name} didn’t make a sandbox adapter.`);
2301
+ return made;
2302
+ }).pipe(tapError(({ cause }) => sync(() => unavailable.set(project, cause instanceof Error ? cause.message : String(cause)))), orElseSucceed(() => null));
2303
+ if (!sandbox) return null;
2304
+ let repository = https ?? project.root;
2305
+ let file = factory.instructions ? join(project.root, factory.instructions) : null;
2306
+ return sandboxExecutor({
2307
+ sandbox,
2308
+ harness,
2309
+ page: previewPage,
2310
+ tools: () => {
2311
+ let origin = sandbox.capabilities.localhost ? origins.get(project) : void 0;
2312
+ return origin ? `${origin}${routes.agentApi.tasks.href()}` : null;
2313
+ },
2314
+ repository,
2315
+ factory,
2316
+ env: keys,
2317
+ secrets: Object.keys(keys),
2318
+ secret: yield* agentSecret(project.root),
2319
+ pushToken,
2320
+ instructions: file ? promise(() => readFile(file, "utf8").catch(() => null)) : void 0
2321
+ });
2322
+ }));
2323
+ }
2324
+ var MAIN_TRACKING = "refs/bud/factory/main";
2325
+ var MERGE_CHECK = 6e4;
2326
+ /**
2327
+ * The project's runs on its task store, picking up runs left going before a restart. Pushed
2328
+ * branches are pictured on the canvas, and a run counts as merged once main contains it.
2329
+ * @param project - The connected project and its factory config.
2330
+ * @param options - How to tell a merge, and how often to check for one.
2331
+ * @returns The runs service; its background work stops with the companion.
2332
+ */
2333
+ function factoryRunsLayer(project, options = {}) {
2334
+ return effect(FactoryRuns, gen(function* () {
2335
+ let factory = project.config.factory;
2336
+ let { work } = yield* AdapterRegistry;
2337
+ if (!factory) return noRuns("The factory is off; turn it on with factory in bud.config.ts.");
2338
+ if (!work) return noRuns("Task storage is off, so the factory has nowhere to keep runs.");
2339
+ let activity = yield* StoreActivity;
2340
+ let executor = yield* RunExecutor;
2341
+ let branches = getOrNull(yield* serviceOption(BranchPreviews));
2342
+ let git = getOrNull(yield* serviceOption(Git));
2343
+ let attachments = getOrNull(yield* serviceOption(Attachments));
2344
+ let images = (taskId) => gen(function* () {
2345
+ if (!attachments || !work.readItemData) return [];
2346
+ let { value } = yield* work.readItemData(project.root, taskId, "budContext");
2347
+ let context = parseTaskContext(value);
2348
+ if (!context?.image) return [];
2349
+ let bytes = yield* attachments.get(context.image);
2350
+ if (!bytes) return [];
2351
+ return [{
2352
+ mime: "image/jpeg",
2353
+ name: "comment.jpg",
2354
+ data: Buffer.from(bytes).toString("base64")
2355
+ }];
2356
+ }).pipe(orElseSucceed(() => []));
2357
+ let picture = (change) => (run) => branches ? asVoid(branches[change](run.branch)) : void_$1;
2358
+ let runs = yield* makeRuns({
2359
+ root: project.root,
2360
+ factory,
2361
+ work,
2362
+ executor,
2363
+ unavailable: unavailable.get(project),
2364
+ images,
2365
+ sleepAfterMs: factory.sleepAfter ? factory.sleepAfter * 6e4 : null,
2366
+ changed: activity.notify("work"),
2367
+ onPushed: picture("activate"),
2368
+ onMerged: picture("deactivate")
2369
+ });
2370
+ yield* forkScoped(ignore(runs.resume));
2371
+ let merged = options.merged;
2372
+ if (!merged && git) {
2373
+ let main = project.config.branches.main ?? "main";
2374
+ let { remote } = factory.push;
2375
+ let local = git;
2376
+ let fetched = 0;
2377
+ merged = (commit) => gen(function* () {
2378
+ let repository = yield* local.discover(project.root);
2379
+ if (Date.now() - fetched > 3e4) {
2380
+ fetched = Date.now();
2381
+ yield* ignore(repository.fetchRef(remote, `refs/heads/${main}`, MAIN_TRACKING));
2382
+ }
2383
+ for (let ref of [MAIN_TRACKING, `refs/heads/${main}`]) {
2384
+ let head = yield* orElseSucceed(repository.resolve(ref), () => null);
2385
+ if (!head) continue;
2386
+ if ((yield* orElseSucceed(repository.mergeBase(commit, head), () => null)) === commit) return true;
2387
+ }
2388
+ return false;
2389
+ }).pipe(orElseSucceed(() => false));
2390
+ }
2391
+ if (executor && merged) {
2392
+ let check = merged;
2393
+ yield* runs.checkMerged(check).pipe(ignore, andThen(sleep(`${options.mergeCheckMs ?? MERGE_CHECK} millis`)), forever, forkScoped);
2394
+ }
2395
+ return runs;
2396
+ }));
2397
+ }
2398
+ //#endregion
1671
2399
  //#region ../files/src/server/errors.ts
1672
2400
  /** A request named an invalid path, or the service was given invalid options. */
1673
2401
  var FilesInputError = class extends TaggedError("FilesInputError") {};
@@ -2352,8 +3080,8 @@ function makeFilesLayer(options, openReader) {
2352
3080
  let confinement = yield* makeConfinement(config.root);
2353
3081
  let scanner = yield* makeScanner(confinement, config.limits);
2354
3082
  let scope$1 = yield* scope;
2355
- let mutex = yield* make$3(1);
2356
- let readers = yield* make$3(config.limits.maxConcurrentReads);
3083
+ let mutex = yield* make$2(1);
3084
+ let readers = yield* make$2(config.limits.maxConcurrentReads);
2357
3085
  let publications = yield* sliding({
2358
3086
  capacity: 1,
2359
3087
  replay: 1
@@ -2420,7 +3148,7 @@ function makeFilesLayer(options, openReader) {
2420
3148
  watcherFault = false;
2421
3149
  for (let [relative, directory] of wanted) {
2422
3150
  if (watchers.has(relative)) continue;
2423
- let ready = yield* make$4();
3151
+ let ready = yield* make$3();
2424
3152
  let state = { active: false };
2425
3153
  let stream = confinement.fs.watch(directory.absolute, {
2426
3154
  recursive: config.recursive,
@@ -2626,7 +3354,7 @@ var FileSearch = class extends Service()("@bud.sh/files/FileSearch") {};
2626
3354
  */
2627
3355
  function fileSearchLayer(options = {}) {
2628
3356
  return effect(FileSearch, gen(function* () {
2629
- let files = yield* Files, mutex = yield* make$3(1);
3357
+ let files = yield* Files, mutex = yield* make$2(1);
2630
3358
  let maxFiles = options.maxFiles ?? 500, maxBytes = options.maxBytes ?? 16777216, timeoutMs = options.timeoutMs ?? 5e3;
2631
3359
  if (![
2632
3360
  maxFiles,
@@ -3906,7 +4634,7 @@ function makeMutationSnapshot(confinement, io, openReader, limits) {
3906
4634
  //#region ../files/src/server/write-lock.ts
3907
4635
  /** One companion's explicit workspace writes share this gate; reads and watchers do not. */
3908
4636
  var FileWriteLock = class extends Service()("@bud.sh/files/FileWriteLock") {};
3909
- var fileWriteLockLayer = effect(FileWriteLock, make$3(1));
4637
+ var fileWriteLockLayer = effect(FileWriteLock, make$2(1));
3910
4638
  //#endregion
3911
4639
  //#region ../files/src/server/mutations.ts
3912
4640
  /** Plans and applies file actions, and lists the recoveries they kept. */
@@ -4028,7 +4756,7 @@ function makeFileMutationsLayer(options, io, openReader) {
4028
4756
  let rootInfo = yield* io.inspect(confinement.root, "");
4029
4757
  let project = hash(`${confinement.root}\0${rootInfo.identity}`);
4030
4758
  let snapshot = makeMutationSnapshot(confinement, io, openReader, config.limits);
4031
- let mutex = yield* make$3(1);
4759
+ let mutex = yield* make$2(1);
4032
4760
  let plans = /* @__PURE__ */ new Map();
4033
4761
  let store;
4034
4762
  function recoveryStore() {
@@ -5477,4 +6205,4 @@ function fileReviewActionsLayer(options) {
5477
6205
  }).pipe(mapError(reviewActionFailure)));
5478
6206
  }
5479
6207
  //#endregion
5480
- export { adapterRegistryLayer as A, sharedModules as B, selected as C, publishChanges as D, liveChannelLayer as E, WorkAdapter as F, make as H, WorkError as I, ScreenModules as L, memoryAdapterLayer as M, parseThreadImport as N, AdapterRegistry as O, ThreadAdapter as P, bundledScreensLayer as R, ready as S, LiveChannel as T, taskDesignModuleId as V, storeActivityLayer as _, FileMutations as a, read as b, FileReview as c, fileSearchLayer as d, Files as f, StoreActivity as g, ReviewActionError as h, fileSavesLayer as i, defineAdapter as j, adapterModuleLoaderLayer as k, fileReviewLayer as l, FilesInputError as m, fileReviewActionsLayer as n, fileMutationsLayer as o, filesLayer as p, FileSaves as r, fileWriteLockLayer as s, FileReviewActions as t, FileSearch as u, storeLiveLayer as v, validate as w, readWorkspace as x, inspect$1 as y, sourceScreensLayer as z };
6208
+ export { liveChannelLayer as A, ThreadAdapter as B, inspect$1 as C, selected as D, ready as E, AdapterRegistry as F, sourceScreensLayer as G, WorkError as H, AdapterStartupError as I, make as J, sharedModules as K, adapterRegistryLayer as L, Attachments as M, attachmentsLayer as N, validate as O, directoryAttachmentStore as P, defineAdapter as R, storeLiveLayer as S, readWorkspace as T, ScreenModules as U, WorkAdapter as V, bundledScreensLayer as W, factoryRunsLayer as _, FileMutations as a, StoreActivity as b, FileReview as c, fileSearchLayer as d, Files as f, FactoryRuns as g, ReviewActionError as h, fileSavesLayer as i, publishChanges as j, LiveChannel as k, fileReviewLayer as l, FilesInputError as m, fileReviewActionsLayer as n, fileMutationsLayer as o, filesLayer as p, taskDesignModuleId as q, FileSaves as r, fileWriteLockLayer as s, FileReviewActions as t, FileSearch as u, rememberOrigin as v, read as w, storeActivityLayer as x, runExecutorLayer as y, parseThreadImport as z };