@shivaedev/work-board 0.9.0 → 0.10.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 (168) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +31 -1
  3. package/assets/native.js +35 -29
  4. package/dist/board.d.ts.map +1 -1
  5. package/dist/board.js +14 -1
  6. package/dist/board.js.map +1 -1
  7. package/dist/browser/client.d.ts +2 -0
  8. package/dist/browser/client.d.ts.map +1 -1
  9. package/dist/browser/client.js +2 -1
  10. package/dist/browser/client.js.map +1 -1
  11. package/dist/browser/entry.d.ts.map +1 -1
  12. package/dist/browser/entry.js +2 -0
  13. package/dist/browser/entry.js.map +1 -1
  14. package/dist/browser/handoffs/copy.d.ts +3 -0
  15. package/dist/browser/handoffs/copy.d.ts.map +1 -0
  16. package/dist/browser/handoffs/copy.js +47 -0
  17. package/dist/browser/handoffs/copy.js.map +1 -0
  18. package/dist/browser/handoffs/fields.d.ts +11 -0
  19. package/dist/browser/handoffs/fields.d.ts.map +1 -0
  20. package/dist/browser/handoffs/fields.js +45 -0
  21. package/dist/browser/handoffs/fields.js.map +1 -0
  22. package/dist/browser/handoffs/schema.d.ts +89 -0
  23. package/dist/browser/handoffs/schema.d.ts.map +1 -0
  24. package/dist/browser/handoffs/schema.js +38 -0
  25. package/dist/browser/handoffs/schema.js.map +1 -0
  26. package/dist/browser/handoffs/setup.d.ts +2 -0
  27. package/dist/browser/handoffs/setup.d.ts.map +1 -0
  28. package/dist/browser/handoffs/setup.js +108 -0
  29. package/dist/browser/handoffs/setup.js.map +1 -0
  30. package/dist/browser/responses/draftSession.d.ts +1 -1
  31. package/dist/browser/responses/draftSession.d.ts.map +1 -1
  32. package/dist/browser/responses/draftSession.js +4 -4
  33. package/dist/browser/responses/draftSession.js.map +1 -1
  34. package/dist/browser/responses/preview.js.map +1 -1
  35. package/dist/cli.js.map +1 -1
  36. package/dist/files/list.d.ts.map +1 -1
  37. package/dist/handoffs/html.d.ts +37 -0
  38. package/dist/handoffs/html.d.ts.map +1 -0
  39. package/dist/handoffs/html.js +31 -0
  40. package/dist/handoffs/html.js.map +1 -0
  41. package/dist/handoffs/page.d.ts +7 -0
  42. package/dist/handoffs/page.d.ts.map +1 -0
  43. package/dist/handoffs/page.js +31 -0
  44. package/dist/handoffs/page.js.map +1 -0
  45. package/dist/handoffs/records.d.ts +7 -0
  46. package/dist/handoffs/records.d.ts.map +1 -0
  47. package/dist/handoffs/records.js +30 -0
  48. package/dist/handoffs/records.js.map +1 -0
  49. package/dist/handoffs/service.d.ts +55 -0
  50. package/dist/handoffs/service.d.ts.map +1 -0
  51. package/dist/handoffs/service.js +82 -0
  52. package/dist/handoffs/service.js.map +1 -0
  53. package/dist/handoffs/source.d.ts +11 -0
  54. package/dist/handoffs/source.d.ts.map +1 -0
  55. package/dist/handoffs/source.js +31 -0
  56. package/dist/handoffs/source.js.map +1 -0
  57. package/dist/http/assets.d.ts.map +1 -1
  58. package/dist/http/assets.js.map +1 -1
  59. package/dist/http/events.d.ts.map +1 -1
  60. package/dist/http/loopback.d.ts.map +1 -1
  61. package/dist/http/page.d.ts.map +1 -1
  62. package/dist/http/respond.d.ts.map +1 -1
  63. package/dist/metadata/backlinks.d.ts.map +1 -1
  64. package/dist/metadata/backlinks.js +1 -0
  65. package/dist/metadata/backlinks.js.map +1 -1
  66. package/dist/metadata/fields.d.ts +14 -1
  67. package/dist/metadata/fields.d.ts.map +1 -1
  68. package/dist/metadata/model.js.map +1 -1
  69. package/dist/metadata/problems.d.ts.map +1 -1
  70. package/dist/metadata/problems.js +1 -0
  71. package/dist/metadata/problems.js.map +1 -1
  72. package/dist/metadata/schema.d.ts +18 -1
  73. package/dist/metadata/schema.d.ts.map +1 -1
  74. package/dist/metadata/schema.js +3 -1
  75. package/dist/metadata/schema.js.map +1 -1
  76. package/dist/page/escape.d.ts.map +1 -1
  77. package/dist/page/metadata.d.ts.map +1 -1
  78. package/dist/page/metadata.js +6 -1
  79. package/dist/page/metadata.js.map +1 -1
  80. package/dist/page/metadataDetails.d.ts.map +1 -1
  81. package/dist/page/metadataDetails.js +3 -0
  82. package/dist/page/metadataDetails.js.map +1 -1
  83. package/dist/page/nav.d.ts.map +1 -1
  84. package/dist/page/navigationScript.js +1 -1
  85. package/dist/page/shell.d.ts +1 -1
  86. package/dist/page/shell.d.ts.map +1 -1
  87. package/dist/page/shell.js.map +1 -1
  88. package/dist/page/startTemplates.d.ts +89 -3
  89. package/dist/page/startTemplates.d.ts.map +1 -1
  90. package/dist/page/swap.d.ts +1 -1
  91. package/dist/page/swap.d.ts.map +1 -1
  92. package/dist/page/swap.js +2 -2
  93. package/dist/page/updated.d.ts.map +1 -1
  94. package/dist/render/board.d.ts.map +1 -1
  95. package/dist/render/callouts.d.ts.map +1 -1
  96. package/dist/render/code.d.ts.map +1 -1
  97. package/dist/render/documentHeadings.d.ts.map +1 -1
  98. package/dist/render/documentLinks.d.ts.map +1 -1
  99. package/dist/render/footnotes.d.ts.map +1 -1
  100. package/dist/render/highlighter.d.ts.map +1 -1
  101. package/dist/render/highlighter.js.map +1 -1
  102. package/dist/render/markdown.js.map +1 -1
  103. package/dist/render/questionnaireDocuments.d.ts.map +1 -1
  104. package/dist/render/visualDocuments.d.ts.map +1 -1
  105. package/dist/responses/agent.d.ts +8 -8
  106. package/dist/responses/agent.js.map +1 -1
  107. package/dist/responses/flags.d.ts.map +1 -1
  108. package/dist/responses/outcome.d.ts.map +1 -1
  109. package/dist/responses/publish.d.ts +1 -1
  110. package/dist/responses/publish.d.ts.map +1 -1
  111. package/dist/responses/publish.js +7 -7
  112. package/dist/responses/publish.js.map +1 -1
  113. package/dist/responses/publishIn.d.ts +1 -1
  114. package/dist/responses/publishIn.d.ts.map +1 -1
  115. package/dist/responses/publishIn.js +3 -3
  116. package/dist/responses/publishIn.js.map +1 -1
  117. package/dist/responses/records.d.ts.map +1 -1
  118. package/dist/responses/records.js +2 -1
  119. package/dist/responses/records.js.map +1 -1
  120. package/dist/responses/waitFor.d.ts.map +1 -1
  121. package/dist/rpc/contract.d.ts +67 -1
  122. package/dist/rpc/contract.d.ts.map +1 -1
  123. package/dist/rpc/contract.js +3 -2
  124. package/dist/rpc/contract.js.map +1 -1
  125. package/dist/rpc/handoffContract.d.ts +142 -0
  126. package/dist/rpc/handoffContract.d.ts.map +1 -0
  127. package/dist/rpc/handoffContract.js +26 -0
  128. package/dist/rpc/handoffContract.js.map +1 -0
  129. package/dist/rpc/server.d.ts +2 -0
  130. package/dist/rpc/server.d.ts.map +1 -1
  131. package/dist/rpc/server.js +4 -1
  132. package/dist/rpc/server.js.map +1 -1
  133. package/dist/search/collector.d.ts.map +1 -1
  134. package/dist/search/entries.js.map +1 -1
  135. package/dist/search/linkCollector.d.ts.map +1 -1
  136. package/dist/serve-cli.js +2 -2
  137. package/dist/serve-cli.js.map +1 -1
  138. package/dist/serve.d.ts.map +1 -1
  139. package/dist/views/state.js.map +1 -1
  140. package/package.json +3 -3
  141. package/src/board.ts +14 -1
  142. package/src/browser/client.ts +3 -1
  143. package/src/browser/entry.ts +2 -0
  144. package/src/browser/handoffs/copy.ts +45 -0
  145. package/src/browser/handoffs/fields.ts +50 -0
  146. package/src/browser/handoffs/schema.ts +44 -0
  147. package/src/browser/handoffs/setup.ts +113 -0
  148. package/src/browser/responses/draftSession.ts +11 -4
  149. package/src/handoffs/html.ts +42 -0
  150. package/src/handoffs/page.ts +45 -0
  151. package/src/handoffs/records.ts +34 -0
  152. package/src/handoffs/service.ts +103 -0
  153. package/src/handoffs/source.ts +35 -0
  154. package/src/metadata/backlinks.ts +1 -0
  155. package/src/metadata/problems.ts +1 -0
  156. package/src/metadata/schema.ts +3 -1
  157. package/src/page/metadata.ts +7 -1
  158. package/src/page/metadataDetails.ts +3 -0
  159. package/src/page/navigationScript.ts +1 -1
  160. package/src/page/shell.ts +1 -1
  161. package/src/page/swap.ts +2 -2
  162. package/src/responses/publish.ts +7 -7
  163. package/src/responses/publishIn.ts +10 -3
  164. package/src/responses/records.ts +4 -1
  165. package/src/rpc/contract.ts +3 -2
  166. package/src/rpc/handoffContract.ts +26 -0
  167. package/src/rpc/server.ts +6 -1
  168. package/src/serve-cli.ts +2 -2
@@ -6,6 +6,12 @@ import { backlinksHtml } from "./backlinksHtml.ts";
6
6
  import { escapeHtml } from "./escape.ts";
7
7
  import { metadataDetails } from "./metadataDetails.ts";
8
8
 
9
+ function handoffLink(id: string | undefined, kind: string | undefined, unique: boolean) {
10
+ return unique && id && !["question", "response", "handoff"].includes(kind ?? "")
11
+ ? `<p><a href="/_board/handoff?item=${encodeURIComponent(id)}">Prepare an agent handoff</a></p>`
12
+ : "";
13
+ }
14
+
9
15
  export function metadataHtml(parsed: ParsedMetadata, file: string, model: MetadataModel): string {
10
16
  const backlinks = backlinksHtml(file, model);
11
17
  const diagnostics = model.diagnostics.get(file) ?? parsed.diagnostics;
@@ -27,7 +33,7 @@ export function metadataHtml(parsed: ParsedMetadata, file: string, model: Metada
27
33
  unique && fields.kind === "board" && fields.id
28
34
  ? `<p><a href="${escapeHtml(viewUrl(stateOf("/"), { board: fields.id }))}">View this board</a></p>`
29
35
  : "";
30
- return `<section class="work-meta" aria-label="Recorded work"><p class="metadata-summary">Recorded work · ${identity}${fields.status ? ` · ${escapeHtml(fields.status)}` : ""}${fields.owner ? ` · ${escapeHtml(fields.owner)}` : ""}</p>${fields.nextAction ? `<p>Next action: ${escapeHtml(fields.nextAction)}</p>` : ""}${boardLink}${problems}<details data-key="work-metadata"><summary>Work details and source</summary>${details}<p>Source: ${escapeHtml(file)}:${parsed.lines.id ?? 1}; prose starts at line ${parsed.bodyLine}.</p>${parsed.raw ? `<details data-key="frontmatter-source"><summary>Original frontmatter</summary><pre>${escapeHtml(parsed.raw)}</pre></details>` : ""}</details></section>${backlinks}`;
36
+ return `<section class="work-meta" aria-label="Recorded work"><p class="metadata-summary">Recorded work · ${identity}${fields.status ? ` · ${escapeHtml(fields.status)}` : ""}${fields.owner ? ` · ${escapeHtml(fields.owner)}` : ""}</p>${fields.nextAction ? `<p>Next action: ${escapeHtml(fields.nextAction)}</p>` : ""}${boardLink}${handoffLink(fields.id, fields.kind, unique)}${problems}<details data-key="work-metadata"><summary>Work details and source</summary>${details}<p>Source: ${escapeHtml(file)}:${parsed.lines.id ?? 1}; prose starts at line ${parsed.bodyLine}.</p>${parsed.raw ? `<details data-key="frontmatter-source"><summary>Original frontmatter</summary><pre>${escapeHtml(parsed.raw)}</pre></details>` : ""}</details></section>${backlinks}`;
31
37
  }
32
38
 
33
39
  export const metadataStyles = `
@@ -10,6 +10,9 @@ import { recordedEvidence } from "./recordedEvidence.ts";
10
10
  export function metadataDetails(parsed: ParsedMetadata, file: string, model: MetadataModel): string {
11
11
  const fields = parsed.fields;
12
12
  const details = [
13
+ fields.handoff
14
+ ? `<h3>Agent handoff</h3><p>${referenceHtml(fields.handoff.item, fields.handoff.item, model)} · ${escapeHtml(fields.handoff.recipient)} · ${escapeHtml(fields.handoff.state)}</p><p>Receipt is separate from execution and acceptance.</p><p>${escapeHtml(fields.handoff.by ?? "Attribution not recorded")}${fields.handoff.note ? ` · ${escapeHtml(fields.handoff.note)}` : ""}</p><p>Reviewed SHA-256: <code>${fields.handoff.reviewedRevision}</code></p><p><a href="/_board/handoff?item=${encodeURIComponent(fields.handoff.item)}">Read handoff history and copy its prompt</a></p>`
15
+ : "",
13
16
  `<dl><dt>Kind</dt><dd>${valueHtml(fields.kind)}</dd><dt>Status</dt><dd>${valueHtml(fields.status)}</dd><dt>Owner</dt><dd>${valueHtml(fields.owner)}</dd><dt>Next action</dt><dd>${valueHtml(fields.nextAction)}</dd></dl>`,
14
17
  fields.relationships?.length
15
18
  ? `<h3>Relationships</h3><ul>${fields.relationships.map((link) => `<li>${escapeHtml(relationshipLabel(link.kind))}: ${referenceHtml(link.target, link.target, model)}</li>`).join("")}</ul>`
@@ -88,7 +88,7 @@ document.addEventListener("click", (event) => {
88
88
  const link = event.target.closest?.("a[href]");
89
89
  if (!link || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || link.hasAttribute("download") || (link.target && link.target !== "_self")) return;
90
90
  const target = new URL(link.href, location.href);
91
- if (target.origin !== location.origin || !(target.pathname === "/" || (target.pathname === "/_board/respond" || target.pathname === "/_board/start" || target.pathname === "/_board/changes" || target.pathname === "/_board/overview" || target.pathname === "/_board/work" || target.pathname.startsWith("/_board/item/")) || /\\.md$/i.test(target.pathname))) return;
91
+ if (target.origin !== location.origin || !(target.pathname === "/" || (target.pathname === "/_board/handoff" || target.pathname === "/_board/respond" || target.pathname === "/_board/start" || target.pathname === "/_board/changes" || target.pathname === "/_board/overview" || target.pathname === "/_board/work" || target.pathname.startsWith("/_board/item/")) || /\\.md$/i.test(target.pathname))) return;
92
92
  event.preventDefault();
93
93
  navigate(target);
94
94
  });
package/src/page/shell.ts CHANGED
@@ -17,7 +17,7 @@ export const shell = (
17
17
  workspace: string,
18
18
  board: boolean,
19
19
  identity?: string,
20
- view: boolean | "overview" | "changes" | "start" | "response" = false,
20
+ view: boolean | "overview" | "changes" | "start" | "response" | "handoff" = false,
21
21
  ): string =>
22
22
  [
23
23
  `<!doctype html><html lang="en" data-workspace="${escapeHtml(workspace)}"><head><meta charset="utf-8">`,
package/src/page/swap.ts CHANGED
@@ -18,8 +18,8 @@ export const remember = (element) => {
18
18
 
19
19
  const normalized = (element) => {
20
20
  const copy = element.cloneNode(true);
21
- for (const form of [copy, ...copy.querySelectorAll("#response-form")]) {
22
- if (form.id === "response-form") { form.replaceChildren(); delete form.dataset.bound; }
21
+ for (const form of [copy, ...copy.querySelectorAll("#response-form, #handoff-form")]) {
22
+ if (form.id === "response-form" || form.id === "handoff-form") { form.replaceChildren(); delete form.dataset.bound; }
23
23
  }
24
24
  for (const tool of copy.querySelectorAll("[data-visual-tools], [data-image-error], [data-diagram-error]")) tool.remove();
25
25
  for (const node of [copy, ...copy.querySelectorAll("*")]) {
@@ -9,20 +9,20 @@ function failure(cause: unknown): ResponseFailed {
9
9
  ? cause
10
10
  : new ResponseFailed({
11
11
  code: "Unavailable",
12
- message: "The response could not be recorded. Keep the draft and retry after checking local filesystem access.",
12
+ message: "The local record could not be recorded. Keep the draft and retry after checking local filesystem access.",
13
13
  });
14
14
  }
15
- export function publish(root: string, realRoot: string, id: string, content: string) {
15
+ export function publish(root: string, realRoot: string, id: string, content: string, namespace: "responses" | "handoffs" = "responses") {
16
16
  return Effect.tryPromise({
17
17
  catch: failure,
18
18
  try: async () => {
19
19
  if (/^[a-zA-Z\d][a-zA-Z\d._-]*$/u.exec(id) === null) {
20
- throw new ResponseFailed({ code: "Conflict", message: "Invalid response identity." });
20
+ throw new ResponseFailed({ code: "Conflict", message: "Invalid record identity." });
21
21
  }
22
22
  if (process.platform !== "linux") {
23
23
  throw new ResponseFailed({
24
24
  code: "Unsupported",
25
- message: "Safe response publication currently requires Linux directory descriptors. Reading remains available.",
25
+ message: "Local record publication currently requires Linux directory descriptors. Reading remains available.",
26
26
  });
27
27
  }
28
28
  const directory = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
@@ -32,16 +32,16 @@ export function publish(root: string, realRoot: string, id: string, content: str
32
32
  throw new ResponseFailed({ code: "Conflict", message: "The workspace root changed. Restart against the intended root." });
33
33
  }
34
34
  try {
35
- await mkdir(`${base}/responses`);
35
+ await mkdir(`${base}/${namespace}`);
36
36
  } catch (cause) {
37
37
  if (!(cause instanceof Error && "code" in cause && cause.code === "EEXIST")) {
38
38
  throw cause;
39
39
  }
40
40
  }
41
41
  await directory.sync();
42
- const target = await open(`${base}/responses`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
42
+ const target = await open(`${base}/${namespace}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
43
43
  try {
44
- await publishIn(target, root, realRoot, id, content);
44
+ await publishIn(target, root, realRoot, id, content, namespace);
45
45
  } finally {
46
46
  await target.close();
47
47
  }
@@ -4,7 +4,7 @@ import { type FileHandle, link, open, readFile, realpath, unlink } from "node:fs
4
4
  import { ResponseFailed } from "#browser/responses/schema.ts";
5
5
 
6
6
  function uncertain(cause: unknown) {
7
- return new ResponseFailed({ cause, code: "Uncertain", message: "Publication may have completed. Re-read the response identity before retrying." });
7
+ return new ResponseFailed({ cause, code: "Uncertain", message: "Publication may have completed. Re-read the record identity before retrying." });
8
8
  }
9
9
  function exists(cause: unknown) {
10
10
  return cause instanceof Error && "code" in cause && cause.code === "EEXIST";
@@ -28,9 +28,16 @@ async function linkOrReconcile(temporary: string, name: string, content: string)
28
28
  }
29
29
  return false;
30
30
  }
31
- export async function publishIn(target: FileHandle, root: string, realRoot: string, id: string, content: string) {
31
+ export async function publishIn(
32
+ target: FileHandle,
33
+ root: string,
34
+ realRoot: string,
35
+ id: string,
36
+ content: string,
37
+ namespace: "responses" | "handoffs" = "responses",
38
+ ) {
32
39
  const folder = `/proc/self/fd/${target.fd}`;
33
- const expected = `${realRoot}/responses`;
40
+ const expected = `${realRoot}/${namespace}`;
34
41
  async function validate() {
35
42
  if ((await realpath(folder)) !== expected || (await realpath(root)) !== realRoot) {
36
43
  throw new ResponseFailed({ code: "Conflict", message: "The write boundary changed." });
@@ -51,5 +51,8 @@ export function matchesResponse(record: RecordedResponse, input: DraftInput) {
51
51
 
52
52
  export function malformedResponse(document: MetadataDocument) {
53
53
  const fields = document.parsed.fields;
54
- return fields.kind === "response" && !(fields.id && fields.response);
54
+ return (
55
+ (fields.kind === "response" && !(fields.id && fields.response))
56
+ || (document.file.startsWith("responses/") && document.parsed.diagnostics.some((problem) => problem.field === undefined))
57
+ );
55
58
  }
@@ -4,6 +4,7 @@ import { contract } from "@shivaedev/effect-contract/contract.ts";
4
4
  import { LiveHint } from "@shivaedev/effect-contract/live.ts";
5
5
  import { query } from "@shivaedev/effect-contract/operation.ts";
6
6
  import { HistoryInput, HistoryOutput } from "#history/schema.ts";
7
+ import { handoffContract } from "#rpc/handoffContract.ts";
7
8
  import { responseContract } from "#rpc/responseContract.ts";
8
9
  import { index, navigation, watcher, workspace } from "./keys.ts";
9
10
  import { pageKeys } from "./pageKeys.ts";
@@ -19,7 +20,7 @@ const PageUrl = Schema.String.check(
19
20
  const path = new URL(value, "http://127.0.0.1").pathname;
20
21
  return (
21
22
  path === "/"
22
- || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond"].includes(path)
23
+ || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond", "/_board/handoff"].includes(path)
23
24
  || /^\/_board\/item\/[^/]+\/$/u.exec(path) !== null
24
25
  || /\.md$/iu.exec(path) !== null
25
26
  || "Expected a local reading URL"
@@ -60,4 +61,4 @@ const ReadWatcher = query("watcher", {
60
61
  });
61
62
  export const workContract = contract("work-board", { queries: [ReadPage, ReadNavigation, ReadSearch, ReadHistory, ReadWatcher] });
62
63
  export const Subscribe = Rpc.make("work-board.subscribe", { error: ReadFailed, stream: true, success: LiveHint });
63
- export const workRpcs = workContract.add(Subscribe).merge(responseContract);
64
+ export const workRpcs = workContract.add(Subscribe).merge(responseContract).merge(handoffContract);
@@ -0,0 +1,26 @@
1
+ import { Schema } from "effect";
2
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
3
+ import { command, query } from "@shivaedev/effect-contract/operation.ts";
4
+ import { Handoff, HandoffInput, HandoffPreview, HandoffReading } from "#browser/handoffs/schema.ts";
5
+ import { ResponseFailed } from "#browser/responses/schema.ts";
6
+ import { index, workspace } from "#rpc/keys.ts";
7
+
8
+ const locate = query("handoffSource", {
9
+ payload: { item: Schema.String },
10
+ reads: () => [workspace.list, index.list],
11
+ rejections: { ResponseFailed },
12
+ success: HandoffPreview,
13
+ });
14
+ const read = query("handoffs", {
15
+ payload: { item: Schema.String },
16
+ reads: () => [workspace.list, index.list],
17
+ rejections: { ResponseFailed },
18
+ success: HandoffReading,
19
+ });
20
+ const prepare = command("prepareHandoff", {
21
+ invalidates: () => [workspace.list, index.list],
22
+ payload: HandoffInput,
23
+ rejections: { ResponseFailed },
24
+ success: Handoff,
25
+ });
26
+ export const handoffContract = contract("work-board", { commands: [prepare], queries: [locate, read] });
package/src/rpc/server.ts CHANGED
@@ -2,6 +2,7 @@ import { Clock, Effect, Layer, Ref } from "effect";
2
2
  import { HttpIncomingMessage, HttpServerResponse } from "effect/unstable/http";
3
3
  import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
4
4
  import type { Changes } from "#files/changes.ts";
5
+ import type { handoffService } from "#handoffs/service.ts";
5
6
  import { MAX_BYTES } from "#history/limits.ts";
6
7
  import { historyResponse } from "#history/response.ts";
7
8
  import { HEADERS } from "#http/respond.ts";
@@ -14,15 +15,18 @@ import { hints } from "./hints.ts";
14
15
 
15
16
  export interface ServerOptions {
16
17
  readonly changes: Changes;
18
+ readonly handoffs: Effect.Success<ReturnType<typeof handoffService>>;
17
19
  readonly home: string | undefined;
18
20
  readonly index: Effect.Effect<Snapshot, unknown>;
19
21
  readonly page: (url: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown>;
20
22
  readonly responses: Effect.Success<ReturnType<typeof responseService>>;
21
23
  }
22
24
 
23
- export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page, responses }: ServerOptions) {
25
+ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, handoffs, home, index, page, responses }: ServerOptions) {
24
26
  const handlers = workRpcs.toLayer({
25
27
  "work-board.awaitResponse": responses.awaitResponse,
28
+ "work-board.handoffSource": ({ item }) => handoffs.locate(item),
29
+ "work-board.handoffs": ({ item }) => handoffs.read(item),
26
30
  "work-board.history": ({ action, baseline }) =>
27
31
  Effect.gen(function* () {
28
32
  const now = yield* Clock.currentTimeMillis;
@@ -43,6 +47,7 @@ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home,
43
47
  }),
44
48
  Effect.catch((error) => Effect.fail(error instanceof ReadFailed ? error : new ReadFailed({ operation: "page", status: 500 }))),
45
49
  ),
50
+ "work-board.prepareHandoff": handoffs.prepare,
46
51
  "work-board.question": ({ item, request }) => responses.locate(item, request),
47
52
  "work-board.recordResponse": responses.record,
48
53
  "work-board.registerQuestion": responses.register,
package/src/serve-cli.ts CHANGED
@@ -4,7 +4,7 @@ import { Effect, Layer, Option, Path } from "effect";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
5
  import { HOST, serveBoard } from "./serve.ts";
6
6
 
7
- const VERSION = "0.9.0";
7
+ const VERSION = "0.10.0";
8
8
 
9
9
  const serve = Effect.fn("WorkBoard.serve")(function* (input: {
10
10
  readonly dir: string;
@@ -26,7 +26,7 @@ const workBoard = Command.make(
26
26
  port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
27
27
  responses: Flag.boolean("responses").pipe(
28
28
  Flag.withDefault(false),
29
- Flag.withDescription("Explicitly enable local durable question and response writes (Linux)"),
29
+ Flag.withDescription("Explicitly enable local durable question, response and handoff writes (Linux)"),
30
30
  ),
31
31
  },
32
32
  serve,