@popoverai/dotrequirements 0.33.0 → 0.34.1

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 (119) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +13 -1
  3. package/dist/cloud.d.ts +5 -1
  4. package/dist/cloud.js +6 -2
  5. package/dist/commands/acceptance-test.js +1 -1
  6. package/dist/commands/board/index.d.ts +13 -0
  7. package/dist/commands/board/index.js +96 -0
  8. package/dist/commands/branch-merge.d.ts +78 -0
  9. package/dist/commands/branch-merge.js +129 -0
  10. package/dist/commands/diff.js +15 -5
  11. package/dist/commands/mockup/index.d.ts +11 -0
  12. package/dist/commands/mockup/index.js +82 -0
  13. package/dist/commands/scores/index.d.ts +50 -0
  14. package/dist/commands/scores/index.js +190 -0
  15. package/dist/commands/solution/ask.d.ts +9 -0
  16. package/dist/commands/solution/ask.js +27 -0
  17. package/dist/commands/solution/author.d.ts +19 -0
  18. package/dist/commands/solution/author.js +202 -0
  19. package/dist/commands/solution/branch.d.ts +103 -0
  20. package/dist/commands/solution/branch.js +341 -0
  21. package/dist/commands/solution/brief.d.ts +81 -0
  22. package/dist/commands/solution/brief.js +47 -0
  23. package/dist/commands/solution/checkout.d.ts +27 -0
  24. package/dist/commands/solution/checkout.js +66 -0
  25. package/dist/commands/solution/client.d.ts +16 -0
  26. package/dist/commands/solution/client.js +46 -0
  27. package/dist/commands/solution/contract.d.ts +9 -0
  28. package/dist/commands/solution/contract.js +23 -0
  29. package/dist/commands/solution/create.d.ts +14 -0
  30. package/dist/commands/solution/create.js +52 -0
  31. package/dist/commands/solution/demo-script.d.ts +28 -0
  32. package/dist/commands/solution/demo-script.js +43 -0
  33. package/dist/commands/solution/image.d.ts +60 -0
  34. package/dist/commands/solution/image.js +148 -0
  35. package/dist/commands/solution/index.d.ts +12 -0
  36. package/dist/commands/solution/index.js +282 -0
  37. package/dist/commands/solution/look-up.d.ts +9 -0
  38. package/dist/commands/solution/look-up.js +69 -0
  39. package/dist/commands/solution/next.d.ts +12 -0
  40. package/dist/commands/solution/next.js +48 -0
  41. package/dist/commands/solution/plan-file.d.ts +22 -0
  42. package/dist/commands/solution/plan-file.js +33 -0
  43. package/dist/commands/solution/preview-screenshot.d.ts +21 -0
  44. package/dist/commands/solution/preview-screenshot.js +56 -0
  45. package/dist/commands/solution/pull-request.d.ts +25 -0
  46. package/dist/commands/solution/pull-request.js +117 -0
  47. package/dist/commands/solution/pushing.d.ts +66 -0
  48. package/dist/commands/solution/pushing.js +185 -0
  49. package/dist/commands/solution/rehearse.d.ts +10 -0
  50. package/dist/commands/solution/rehearse.js +24 -0
  51. package/dist/commands/solution/request.d.ts +3 -0
  52. package/dist/commands/solution/request.js +13 -0
  53. package/dist/commands/solution/review.d.ts +27 -0
  54. package/dist/commands/solution/review.js +104 -0
  55. package/dist/commands/solution/run-file.d.ts +43 -0
  56. package/dist/commands/solution/run-file.js +69 -0
  57. package/dist/commands/solution/share.d.ts +62 -0
  58. package/dist/commands/solution/share.js +346 -0
  59. package/dist/commands/solution/stall.d.ts +14 -0
  60. package/dist/commands/solution/stall.js +41 -0
  61. package/dist/commands/solution/start.d.ts +14 -0
  62. package/dist/commands/solution/start.js +31 -0
  63. package/dist/commands/solution/status.d.ts +39 -0
  64. package/dist/commands/solution/status.js +145 -0
  65. package/dist/commands/solution/summary.d.ts +13 -0
  66. package/dist/commands/solution/summary.js +5 -0
  67. package/dist/commands/solution/things.d.ts +118 -0
  68. package/dist/commands/solution/things.js +78 -0
  69. package/dist/commands/solution/thread.d.ts +13 -0
  70. package/dist/commands/solution/thread.js +53 -0
  71. package/dist/commands/solution/upload.d.ts +24 -0
  72. package/dist/commands/solution/upload.js +27 -0
  73. package/dist/commands/solution/video.d.ts +88 -0
  74. package/dist/commands/solution/video.js +364 -0
  75. package/dist/commands/solution/working-folder.d.ts +44 -0
  76. package/dist/commands/solution/working-folder.js +64 -0
  77. package/dist/commands/sync.d.ts +2 -1
  78. package/dist/commands/sync.js +21 -5
  79. package/dist/convex.d.ts +54 -0
  80. package/dist/convex.js +64 -0
  81. package/dist/push/core.js +7 -2
  82. package/dist/rehearsal/derive.d.ts +23 -0
  83. package/dist/rehearsal/derive.js +112 -0
  84. package/dist/rehearsal/driver.d.ts +44 -0
  85. package/dist/rehearsal/driver.js +337 -0
  86. package/dist/rehearsal/earlier.d.ts +25 -0
  87. package/dist/rehearsal/earlier.js +138 -0
  88. package/dist/rehearsal/index.d.ts +5 -0
  89. package/dist/rehearsal/index.js +4 -0
  90. package/dist/rehearsal/live.d.ts +72 -0
  91. package/dist/rehearsal/live.js +301 -0
  92. package/dist/rehearsal/report.d.ts +4 -0
  93. package/dist/rehearsal/report.js +69 -0
  94. package/dist/rehearsal/types.d.ts +377 -0
  95. package/dist/rehearsal/types.js +8 -0
  96. package/dist/requirements/index.d.ts +6 -0
  97. package/dist/requirements/index.js +24 -0
  98. package/dist/schema/browser.d.ts +2 -1
  99. package/dist/schema/browser.js +6 -1
  100. package/dist/schema/file-writer.d.ts +5 -0
  101. package/dist/schema/file-writer.js +4 -0
  102. package/dist/schema/parser.d.ts +11 -2
  103. package/dist/schema/parser.js +11 -7
  104. package/dist/schema/provenance.d.ts +27 -0
  105. package/dist/schema/provenance.js +66 -0
  106. package/dist/schema/schemas.d.ts +40 -0
  107. package/dist/schema/schemas.js +4 -0
  108. package/dist/sync/execute.d.ts +10 -1
  109. package/dist/sync/execute.js +68 -5
  110. package/dist/sync/index.d.ts +1 -1
  111. package/dist/sync/index.js +1 -1
  112. package/dist/sync/publish.d.ts +7 -0
  113. package/dist/sync/segment.js +4 -1
  114. package/dist/sync/snapshot.d.ts +23 -1
  115. package/dist/sync/snapshot.js +75 -3
  116. package/dist/sync/solution-documents.d.ts +74 -0
  117. package/dist/sync/solution-documents.js +135 -0
  118. package/dist/sync/types.d.ts +20 -0
  119. package/package.json +3 -2
@@ -0,0 +1,69 @@
1
+ import { api } from "../../convex.js";
2
+ import { AXES } from "../scores/index.js";
3
+ import { solutionClient } from "./client.js";
4
+ import { requireSolution } from "./run-file.js";
5
+ /**
6
+ * A thing as the reference the verbs take, then how to read it, as the
7
+ * brief lists what an outcome works from (`referenceOf` in the backend's
8
+ * solutions/brief.ts): a document by the command that writes it into the
9
+ * repository, a board and a mockup by the commands that show them, an image
10
+ * by where its copy loads from, a video and a demo's script by their
11
+ * script, and scores as their seven axes.
12
+ */
13
+ function referenceOf(thing) {
14
+ const titled = thing.title ? ` (${thing.title})` : "";
15
+ switch (thing.kind) {
16
+ case "document":
17
+ return `document:${thing.documentId}${titled} — \`dotreq sync --cloud-contributes ${thing.documentId}\` writes it into the repository; read that file`;
18
+ case "board":
19
+ return `board:${thing.boardId}${titled} — \`dotreq board show ${thing.boardId}\``;
20
+ case "mockup":
21
+ return `mockup:${thing.mockupId}${titled} — \`dotreq mockup preview ${thing.mockupId} --out mockup.html\``;
22
+ case "file":
23
+ return `file:${thing.path}${thing.branch ? `@${thing.branch}` : ""}${titled}`;
24
+ case "link":
25
+ return `link:${thing.url}${titled}`;
26
+ case "preview":
27
+ return `"preview:${thing.port}${thing.page ?? ""} ${thing.command}"${titled}`;
28
+ case "pull request":
29
+ return `pr:${thing.number}${titled} — ${thing.url}`;
30
+ case "image":
31
+ return `image ${thing.name}${titled} — ${thing.url ?? "its copy is gone"}`;
32
+ case "video":
33
+ case "demo script":
34
+ return `${thing.kind} ${thing.name}${titled}, recorded from this script:\n\n${(thing.script ?? "").replace(/^/gm, " ")}`;
35
+ case "scores":
36
+ return AXES.map((axis) => {
37
+ const score = thing[axis.key];
38
+ return `${axis.label}: ${score?.score}/5 — ${score?.reason}`;
39
+ }).join("\n- ");
40
+ default:
41
+ return JSON.stringify(thing);
42
+ }
43
+ }
44
+ /**
45
+ * `dotreq solution things <name>`: what an outcome, an input, or the
46
+ * outcomes a checkpoint presents hold now, one reference a line, under
47
+ * each outcome's name. Where a brief says what else the solution has made,
48
+ * to look up if it matters, it names this command.
49
+ */
50
+ export async function thingsCommand(name, options) {
51
+ const { solutionId } = requireSolution(options);
52
+ const client = solutionClient();
53
+ const looked = await client.query(api.solutions.queries.getThings, {
54
+ solutionId,
55
+ name,
56
+ });
57
+ if (!looked) {
58
+ throw new Error(`The solution has no outcome, input or checkpoint called "${name}".`);
59
+ }
60
+ const sections = looked.made.map(({ outcomes, things }) => [
61
+ `### ${outcomes.join(", ")}`,
62
+ "",
63
+ ...(things.length > 0
64
+ ? things.map((thing) => `- ${referenceOf(thing)}`)
65
+ : ["Nothing yet."]),
66
+ ].join("\n"));
67
+ process.stdout.write(`${sections.join("\n\n")}\n`);
68
+ }
69
+ //# sourceMappingURL=look-up.js.map
@@ -0,0 +1,12 @@
1
+ import { type WhereOptions } from "./run-file.js";
2
+ /**
3
+ * `dotreq solution next`: what an agent runs when it is invoked. Which
4
+ * solution and agent is the run's own (VERB-4); the backend takes the
5
+ * first part of an outcome's work the agent has due — drafting an outcome
6
+ * it drafts, or reviewing one it reviews — and composes its brief
7
+ * (PROMPT-BLOCK-6), which this prints as given.
8
+ */
9
+ export declare function nextCommand(options: WhereOptions & {
10
+ json?: boolean;
11
+ }): Promise<void>;
12
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { api } from "../../convex.js";
2
+ import { EXIT, renderStanding } from "./brief.js";
3
+ import { solutionClient } from "./client.js";
4
+ import { saveDemoScriptCopies } from "./demo-script.js";
5
+ import { saveImageCopies } from "./image.js";
6
+ import { requireWhere } from "./run-file.js";
7
+ import { layOut } from "./working-folder.js";
8
+ /**
9
+ * `dotreq solution next`: what an agent runs when it is invoked. Which
10
+ * solution and agent is the run's own (VERB-4); the backend takes the
11
+ * first part of an outcome's work the agent has due — drafting an outcome
12
+ * it drafts, or reviewing one it reviews — and composes its brief
13
+ * (PROMPT-BLOCK-6), which this prints as given.
14
+ */
15
+ export async function nextCommand(options) {
16
+ const { solutionId, agent } = requireWhere(options);
17
+ const client = solutionClient();
18
+ const result = await client.mutation(api.solutions.mutations.next, { solutionId, agent });
19
+ // With --json: the kind, the outcome, and the same text, for a workflow
20
+ // to parse; `step` repeats the outcome for a reader that knows it by that name.
21
+ const emit = (outcome, text) => {
22
+ process.stdout.write(options.json
23
+ ? `${JSON.stringify({ kind: result.kind, outcome, step: outcome, text })}\n`
24
+ : text);
25
+ };
26
+ if (result.kind === "nothing ready") {
27
+ emit(null, renderStanding(result));
28
+ process.exitCode = EXIT.nothingReady;
29
+ return;
30
+ }
31
+ // BRANCH-4.0: on a project with no repository connected, the documents the
32
+ // outcome works from are written into the working folder before it starts.
33
+ const folder = result
34
+ .workingFolder;
35
+ if (folder?.length)
36
+ layOut(folder);
37
+ emit(result.step.outcome ?? result.step.label, result.text.endsWith("\n") ? result.text : `${result.text}\n`);
38
+ // SIGHT-13.1: a demo's script the reviewer reads is saved in its
39
+ // checkout, where its brief says it is, to record the video from.
40
+ saveDemoScriptCopies(result.reviewing ?? []);
41
+ // SIGHT-9.3: the images the outcome works on are saved in the checkout,
42
+ // where its brief says they are.
43
+ await saveImageCopies(client, solutionId, [
44
+ ...(result.reviewing ?? []),
45
+ ...(result.against ?? []),
46
+ ]);
47
+ }
48
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1,22 @@
1
+ /** A plan or scenario file, read as YAML (or JSON, which YAML reads too). */
2
+ export declare function readPlanFile(text: string, _file: string): unknown;
3
+ /**
4
+ * A plan file's plan as the backend takes it (docs/working/plan-surfaces.md):
5
+ * its surfaces, its phases of outcomes under `plan`, its approvals, agents,
6
+ * loops, inputs, `orient to` and hooks, as the file writes them; the backend reads them
7
+ * (`readOutcomePlan`). A file whose `plan` isn't a list of phases, as a
8
+ * scenario's `{ from: default, ... }`, goes as it is.
9
+ */
10
+ export declare function planPayload(spec: {
11
+ surfaces?: unknown;
12
+ plan?: unknown;
13
+ approvals?: unknown;
14
+ checkpoints?: unknown;
15
+ agents?: unknown;
16
+ inputs?: unknown;
17
+ loops?: unknown;
18
+ "orient to"?: unknown;
19
+ conversations?: unknown;
20
+ hooks?: unknown;
21
+ }): unknown;
22
+ //# sourceMappingURL=plan-file.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { parse } from "yaml";
2
+ /** A plan or scenario file, read as YAML (or JSON, which YAML reads too). */
3
+ export function readPlanFile(text, _file) {
4
+ return parse(text);
5
+ }
6
+ /**
7
+ * A plan file's plan as the backend takes it (docs/working/plan-surfaces.md):
8
+ * its surfaces, its phases of outcomes under `plan`, its approvals, agents,
9
+ * loops, inputs, `orient to` and hooks, as the file writes them; the backend reads them
10
+ * (`readOutcomePlan`). A file whose `plan` isn't a list of phases, as a
11
+ * scenario's `{ from: default, ... }`, goes as it is.
12
+ */
13
+ export function planPayload(spec) {
14
+ if (!Array.isArray(spec.plan))
15
+ return spec.plan;
16
+ const payload = { plan: spec.plan };
17
+ for (const key of [
18
+ "surfaces",
19
+ "approvals",
20
+ "checkpoints",
21
+ "agents",
22
+ "inputs",
23
+ "loops",
24
+ "orient to",
25
+ "conversations",
26
+ "hooks",
27
+ ]) {
28
+ if (spec[key] !== undefined)
29
+ payload[key] = spec[key];
30
+ }
31
+ return payload;
32
+ }
33
+ //# sourceMappingURL=plan-file.js.map
@@ -0,0 +1,21 @@
1
+ import { type Image } from "./image.js";
2
+ /**
3
+ * A screenshot of the page a preview opens on (SIGHT-12.0), taken where
4
+ * the drafter submits it, with the preview running on this machine:
5
+ * `agent-browser`, which the runner installs, opens the page in a session
6
+ * of its own, so the agent's own browser is left as it was, and the image
7
+ * is read as an image submitted is. When it can't be taken — nothing
8
+ * answers on the port, or there is no `agent-browser` here — the preview is
9
+ * submitted without one and the tab says so (12.2); `missed` says why, for
10
+ * the agent. The page is asked for at 127.0.0.1, as the preview's gate
11
+ * asks for it.
12
+ */
13
+ export declare function screenshotPreview(preview: {
14
+ port: number;
15
+ page?: string;
16
+ }): Promise<{
17
+ image: Image;
18
+ } | {
19
+ missed: string;
20
+ }>;
21
+ //# sourceMappingURL=preview-screenshot.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { readImage } from "./image.js";
7
+ const run = promisify(execFile);
8
+ /** How long the page has to answer, a first compile included, before no screenshot is taken. */
9
+ const ANSWER_MS = 60_000;
10
+ /**
11
+ * A screenshot of the page a preview opens on (SIGHT-12.0), taken where
12
+ * the drafter submits it, with the preview running on this machine:
13
+ * `agent-browser`, which the runner installs, opens the page in a session
14
+ * of its own, so the agent's own browser is left as it was, and the image
15
+ * is read as an image submitted is. When it can't be taken — nothing
16
+ * answers on the port, or there is no `agent-browser` here — the preview is
17
+ * submitted without one and the tab says so (12.2); `missed` says why, for
18
+ * the agent. The page is asked for at 127.0.0.1, as the preview's gate
19
+ * asks for it.
20
+ */
21
+ export async function screenshotPreview(preview) {
22
+ const url = `http://127.0.0.1:${preview.port}${preview.page ?? "/"}`;
23
+ try {
24
+ const answer = await fetch(url, {
25
+ signal: AbortSignal.timeout(ANSWER_MS),
26
+ });
27
+ await answer.body?.cancel();
28
+ }
29
+ catch {
30
+ return { missed: `nothing answered at ${url}` };
31
+ }
32
+ const dir = mkdtempSync(join(tmpdir(), "dotreq-preview-"));
33
+ const session = `dotreq-preview-${preview.port}`;
34
+ const browse = (...args) => run("agent-browser", ["--session", session, ...args], {
35
+ timeout: ANSWER_MS,
36
+ });
37
+ try {
38
+ const path = join(dir, `preview-${preview.port}.png`);
39
+ await browse("open", url);
40
+ await browse("screenshot", path);
41
+ return { image: readImage(path) };
42
+ }
43
+ catch (error) {
44
+ const code = error.code;
45
+ return {
46
+ missed: code === "ENOENT"
47
+ ? "agent-browser isn't installed here"
48
+ : `agent-browser couldn't take it: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`,
49
+ };
50
+ }
51
+ finally {
52
+ await browse("close").catch(() => undefined);
53
+ rmSync(dir, { recursive: true, force: true });
54
+ }
55
+ }
56
+ //# sourceMappingURL=preview-screenshot.js.map
@@ -0,0 +1,25 @@
1
+ import { type Where, type WhereOptions } from "./run-file.js";
2
+ /** The solution's pull request: its number and where it opens. */
3
+ export type PullRequest = {
4
+ url: string;
5
+ number: number;
6
+ };
7
+ /** The command that opens the solution's pull request and prints its reference. */
8
+ export declare const OPEN_IT = "dotreq solution pull-request";
9
+ /**
10
+ * `dotreq solution pull-request`: the solution's pull request, opened or
11
+ * found (PULL-2). Inside a runner the branch is pushed first, as a submission
12
+ * pushes it (2.0, SOLBRANCH-3); then the app opens a pull request from it
13
+ * into the branch it was made from, or finds the one already open (2.3,
14
+ * 2.4), and Ari is given its reference and its address (2.1). Nothing is
15
+ * recorded: Ari submits it with its work, by the reference (PULL-3.0).
16
+ */
17
+ export declare function pullRequestCommand(options: WhereOptions): Promise<void>;
18
+ /**
19
+ * The pull request Ari submitted as `pr:<number>`, checked against the
20
+ * repository: the one open from the solution's branch, with its address
21
+ * (PULL-3.0). Any other is refused, naming the open one by its reference,
22
+ * or, when none is open, the command that opens it (3.2).
23
+ */
24
+ export declare function checkPullRequest(where: Pick<Where, "solutionId" | "agent" | "outcome">, number: number): Promise<PullRequest>;
25
+ //# sourceMappingURL=pull-request.d.ts.map
@@ -0,0 +1,117 @@
1
+ import { webApiUrl } from "../../cloud.js";
2
+ import { getProjectCredentials } from "../../utils/project-settings.js";
3
+ import { solutionClient } from "./client.js";
4
+ import { pushWork } from "./pushing.js";
5
+ import { requireWhere } from "./run-file.js";
6
+ import { outcomeFor } from "./status.js";
7
+ /** The command that opens the solution's pull request and prints its reference. */
8
+ export const OPEN_IT = "dotreq solution pull-request";
9
+ /**
10
+ * `dotreq solution pull-request`: the solution's pull request, opened or
11
+ * found (PULL-2). Inside a runner the branch is pushed first, as a submission
12
+ * pushes it (2.0, SOLBRANCH-3); then the app opens a pull request from it
13
+ * into the branch it was made from, or finds the one already open (2.3,
14
+ * 2.4), and Ari is given its reference and its address (2.1). Nothing is
15
+ * recorded: Ari submits it with its work, by the reference (PULL-3.0).
16
+ */
17
+ export async function pullRequestCommand(options) {
18
+ const where = requireWhere(options);
19
+ if (where.branch) {
20
+ const pushed = await pushWork(solutionClient(), {
21
+ solutionId: where.solutionId,
22
+ branch: where.branch,
23
+ });
24
+ if (!pushed.ok) {
25
+ throw new Error(pushed.tell
26
+ .replace(/^Nothing was recorded:/, "No pull request was opened:")
27
+ .replace(/record again\.$/, "ask for the pull request again."));
28
+ }
29
+ }
30
+ // The outcome the pull request is for: the one the agent is drafting.
31
+ const outcome = await outcomeFor(solutionClient(), where, "outcome the pull request is for");
32
+ const said = await ask({
33
+ solutionId: where.solutionId,
34
+ agent: where.agent,
35
+ outcome,
36
+ });
37
+ const unopened = "No pull request was opened";
38
+ if (said.noRepository) {
39
+ throw new Error(`${unopened}: a pull request needs a repository, and this project has none connected. Connect one to the project, then ask again.`);
40
+ }
41
+ if (said.noBranch) {
42
+ throw new Error(`${unopened}: the solution has no branch yet. Its first session in a runner makes it.`);
43
+ }
44
+ if (said.notMade) {
45
+ throw new Error(`${unopened}: "${outcome}" isn't kept on a pull request, so it makes none.`);
46
+ }
47
+ if (said.refused) {
48
+ throw new Error(`${unopened}: the repository refused it: ${said.refused}`);
49
+ }
50
+ const pullRequest = pullRequestIn(said);
51
+ if (!pullRequest) {
52
+ throw new Error(`${unopened}: the app gave no pull request.`);
53
+ }
54
+ console.log([
55
+ `The solution's pull request is pr:${pullRequest.number}: ${pullRequest.url}`,
56
+ `Submit it with the rest of your work: dotreq solution author pr:${pullRequest.number} <reference>... --comment "..."`,
57
+ ].join("\n"));
58
+ }
59
+ /**
60
+ * The pull request Ari submitted as `pr:<number>`, checked against the
61
+ * repository: the one open from the solution's branch, with its address
62
+ * (PULL-3.0). Any other is refused, naming the open one by its reference,
63
+ * or, when none is open, the command that opens it (3.2).
64
+ */
65
+ export async function checkPullRequest(where, number) {
66
+ const said = await ask({
67
+ solutionId: where.solutionId,
68
+ agent: where.agent,
69
+ ...(where.outcome ? { outcome: where.outcome } : {}),
70
+ number,
71
+ });
72
+ const unrecorded = "Nothing was recorded";
73
+ if (said.noRepository) {
74
+ throw new Error(`${unrecorded}: a pull request needs a repository, and this project has none connected.`);
75
+ }
76
+ if (said.noBranch) {
77
+ throw new Error(`${unrecorded}: pr:${number} isn't the solution's pull request: the solution has no branch yet.`);
78
+ }
79
+ if (said.refused) {
80
+ throw new Error(`${unrecorded}: couldn't read the solution's pull request from the repository: ${said.refused}`);
81
+ }
82
+ if (said.notOpen !== undefined) {
83
+ throw new Error(said.notOpen
84
+ ? `${unrecorded}: pr:${number} isn't the solution's pull request. The one open from its branch is pr:${said.notOpen.number} (${said.notOpen.url}); submit that one.`
85
+ : `${unrecorded}: pr:${number} isn't the solution's pull request, and none is open from its branch. Open it with \`${OPEN_IT}\`, which prints its reference, and submit it.`);
86
+ }
87
+ const pullRequest = pullRequestIn(said);
88
+ if (!pullRequest) {
89
+ throw new Error(`${unrecorded}: the app gave no pull request.`);
90
+ }
91
+ return pullRequest;
92
+ }
93
+ function pullRequestIn(said) {
94
+ return said.url && typeof said.number === "number"
95
+ ? { url: said.url, number: said.number }
96
+ : null;
97
+ }
98
+ /**
99
+ * Ask the app's pull-request route, on the project's credentials: with the
100
+ * outcome, to open the pull request it is kept on; with a number, to check
101
+ * that one is the solution's open pull request, which needs no outcome.
102
+ */
103
+ async function ask(body) {
104
+ const { projectId, projectSecret } = getProjectCredentials();
105
+ const base = webApiUrl();
106
+ const response = await fetch(`${base}/api/solutions/pull-request`, {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({ projectId, projectSecret, ...body }),
110
+ });
111
+ const said = (await response.json().catch(() => ({})));
112
+ if (!response.ok) {
113
+ throw new Error(`Couldn't ask the app for the solution's pull request: ${said.error ?? `the app answered ${response.status}`}.`);
114
+ }
115
+ return said;
116
+ }
117
+ //# sourceMappingURL=pull-request.js.map
@@ -0,0 +1,66 @@
1
+ import type { solutionClient } from "./client.js";
2
+ import type { Where } from "./run-file.js";
3
+ /**
4
+ * A solution's branch as the verbs work it inside a runner (SOLBRANCH):
5
+ * the checkout the runner starts each session with, the push before a
6
+ * record is kept, the push when a session ends, and what a submission is
7
+ * refused for. Outside a runner the run names no branch and none of this
8
+ * happens: a person pushes their own branches.
9
+ */
10
+ type Client = ReturnType<typeof solutionClient>;
11
+ /**
12
+ * Whether the agent's work reached the solution's branch. When it didn't,
13
+ * `why` fits after "the work could not be pushed:", for a stall's reason
14
+ * (SOLBRANCH-3.4, 4.1), and `tell` is what the agent is told when its
15
+ * record is refused (3.2, 3.3).
16
+ */
17
+ export type PushOutcome = {
18
+ ok: true;
19
+ pushed: boolean;
20
+ } | {
21
+ ok: false;
22
+ why: string;
23
+ tell: string;
24
+ };
25
+ /**
26
+ * Push what the agent committed to the solution's branch, its commits
27
+ * after any other agents pushed since, and keep the push on the solution
28
+ * (SOLBRANCH-3.0, 3.1, 4.0; REHEARSE-6.8). Nothing is committed here: what
29
+ * the agent left uncommitted stays so (SOLBRANCH-6.0).
30
+ *
31
+ * A checkout on another branch than the solution's has nothing it may
32
+ * push: its commits are refused until they are on the solution's branch,
33
+ * and a checkout with none of its own passes.
34
+ */
35
+ export declare function pushWork(client: Client, where: {
36
+ solutionId: string;
37
+ branch: string;
38
+ }, cwd?: string): Promise<PushOutcome>;
39
+ /** Push before a record, or refuse it with what the agent is to do (SOLBRANCH-3). */
40
+ export declare function pushBeforeRecord(client: Client, where: Where): Promise<void>;
41
+ /**
42
+ * What a submission is refused for inside a runner (SOLBRANCH-5): a file it
43
+ * submits that has changes not committed, or is new and not committed
44
+ * (5.0), and other files git tracks with changes not committed (5.1). New
45
+ * files it neither commits nor submits, such as a screenshot, pass (5.2).
46
+ * And a checkout on another branch than the solution's, since a file is
47
+ * recorded on the branch the checkout is on (VERB-1.4).
48
+ */
49
+ export declare function refuseUncommitted(where: Where & {
50
+ branch: string;
51
+ }, submitted: string[], cwd?: string): void;
52
+ /**
53
+ * The checkout a session starts with (SOLBRANCH-1, 2): on the solution's
54
+ * branch — made now, on its first session, from the default branch, or
55
+ * from the branch a rehearsal's scenario named (REHEARSE-4.5) — with what
56
+ * other agents pushed to it brought in. Returns the branch, and what the
57
+ * agent is to be told before its brief, if anything.
58
+ */
59
+ export declare function checkOutSolution(client: Client, solutionId: string, cwd?: string,
60
+ /** The outcome the session works, whose surface's branch it is on (SOLBRANCH-1.5). */
61
+ outcome?: string): Promise<{
62
+ branch: string;
63
+ notice: string | null;
64
+ }>;
65
+ export {};
66
+ //# sourceMappingURL=pushing.d.ts.map
@@ -0,0 +1,185 @@
1
+ import { api } from "../../convex.js";
2
+ import { branchCommit, branchHead, checkOut, currentBranch, defaultBranchCommit, freeName, git, pushTo, remoteBranches, uncommitted, } from "./branch.js";
3
+ /**
4
+ * Push what the agent committed to the solution's branch, its commits
5
+ * after any other agents pushed since, and keep the push on the solution
6
+ * (SOLBRANCH-3.0, 3.1, 4.0; REHEARSE-6.8). Nothing is committed here: what
7
+ * the agent left uncommitted stays so (SOLBRANCH-6.0).
8
+ *
9
+ * A checkout on another branch than the solution's has nothing it may
10
+ * push: its commits are refused until they are on the solution's branch,
11
+ * and a checkout with none of its own passes.
12
+ */
13
+ export async function pushWork(client, where, cwd = process.cwd()) {
14
+ const { branch } = where;
15
+ const on = currentBranch(cwd);
16
+ if (on !== branch) {
17
+ if (!hasOwnCommits(cwd, branch))
18
+ return { ok: true, pushed: false };
19
+ return offBranch(on, branch);
20
+ }
21
+ const pushed = pushTo(cwd, branch);
22
+ switch (pushed.kind) {
23
+ case "pushed":
24
+ await client.mutation(api.solutions.branches.recordPush, {
25
+ solutionId: where.solutionId,
26
+ branch,
27
+ before: pushed.before,
28
+ after: pushed.after,
29
+ });
30
+ return { ok: true, pushed: true };
31
+ case "nothing to push":
32
+ return { ok: true, pushed: false };
33
+ case "conflict": {
34
+ const files = pushed.files.join(", ");
35
+ return {
36
+ ok: false,
37
+ why: `commits other agents pushed to ${branch} conflict with it in ${files}`,
38
+ tell: [
39
+ `Nothing was recorded: commits other agents pushed to ${branch} conflict with yours in ${files}.`,
40
+ `Bring the branch's commits in (git pull --rebase --autostash origin ${branch}), resolve the conflicts, and record again.`,
41
+ ].join("\n"),
42
+ };
43
+ }
44
+ case "refused":
45
+ return {
46
+ ok: false,
47
+ why: `the repository refused it: ${pushed.reason}`,
48
+ tell: `Nothing was recorded: the repository refused the push to ${branch}: ${pushed.reason}`,
49
+ };
50
+ }
51
+ }
52
+ /** Whether the checkout has commits the solution's branch doesn't. */
53
+ function hasOwnCommits(cwd, branch) {
54
+ fetchQuietly(cwd, branch);
55
+ const upstream = `refs/remotes/origin/${branch}`;
56
+ const ran = git(cwd, ["rev-list", "--count", `${upstream}..HEAD`]);
57
+ return ran.code !== 0 || Number(ran.stdout.trim()) > 0;
58
+ }
59
+ /** Fetch the branch without touching the checkout. */
60
+ function fetchQuietly(cwd, branch) {
61
+ git(cwd, [
62
+ "fetch",
63
+ "--quiet",
64
+ "origin",
65
+ `+refs/heads/${branch}:refs/remotes/origin/${branch}`,
66
+ ]);
67
+ }
68
+ function offBranch(on, branch) {
69
+ const where = on ? `on ${on}` : "on no branch";
70
+ return {
71
+ ok: false,
72
+ why: `the checkout is ${where}, not the solution's branch ${branch}`,
73
+ tell: [
74
+ `Nothing was recorded: your checkout is ${where}, not the solution's branch ${branch}, which is where your work is pushed and read from.`,
75
+ `Switch back to ${branch} and bring your commits there (git checkout ${branch} && git merge ${on ?? "<your commit>"}), then record again.`,
76
+ ].join("\n"),
77
+ };
78
+ }
79
+ /** Push before a record, or refuse it with what the agent is to do (SOLBRANCH-3). */
80
+ export async function pushBeforeRecord(client, where) {
81
+ if (!where.branch)
82
+ return;
83
+ const outcome = await pushWork(client, {
84
+ solutionId: where.solutionId,
85
+ branch: where.branch,
86
+ });
87
+ if (!outcome.ok)
88
+ throw new Error(outcome.tell);
89
+ }
90
+ /**
91
+ * What a submission is refused for inside a runner (SOLBRANCH-5): a file it
92
+ * submits that has changes not committed, or is new and not committed
93
+ * (5.0), and other files git tracks with changes not committed (5.1). New
94
+ * files it neither commits nor submits, such as a screenshot, pass (5.2).
95
+ * And a checkout on another branch than the solution's, since a file is
96
+ * recorded on the branch the checkout is on (VERB-1.4).
97
+ */
98
+ export function refuseUncommitted(where, submitted, cwd = process.cwd()) {
99
+ const on = currentBranch(cwd);
100
+ if (on !== where.branch) {
101
+ throw new Error(offBranch(on, where.branch).tell);
102
+ }
103
+ const { changed, untracked } = uncommitted(cwd);
104
+ const within = (file, path) => file === path || file.startsWith(`${path.replace(/\/+$/, "")}/`);
105
+ const paths = submitted.map((path) => path.replace(/^\.\//, ""));
106
+ const submittedFile = (file) => paths.some((path) => within(file, path));
107
+ const notCommitted = [...changed, ...untracked].filter(submittedFile);
108
+ if (notCommitted.length > 0) {
109
+ throw new Error(`Nothing was recorded: commit ${listed(notCommitted)} first. Your work is read from ${where.branch}, where only what you commit is pushed.`);
110
+ }
111
+ const others = changed.filter((file) => !submittedFile(file));
112
+ if (others.length > 0) {
113
+ throw new Error(`Nothing was recorded: ${listed(others)} ${others.length === 1 ? "has changes" : "have changes"} you haven't committed. Commit them or undo them first, then submit again.`);
114
+ }
115
+ }
116
+ /**
117
+ * The checkout a session starts with (SOLBRANCH-1, 2): on the solution's
118
+ * branch — made now, on its first session, from the default branch, or
119
+ * from the branch a rehearsal's scenario named (REHEARSE-4.5) — with what
120
+ * other agents pushed to it brought in. Returns the branch, and what the
121
+ * agent is to be told before its brief, if anything.
122
+ */
123
+ export async function checkOutSolution(client, solutionId, cwd = process.cwd(),
124
+ /** The outcome the session works, whose surface's branch it is on (SOLBRANCH-1.5). */
125
+ outcome) {
126
+ const recorded = await client.query(api.solutions.branches.branchOf, { solutionId, ...(outcome ? { outcome } : {}) });
127
+ const branch = recorded.branch;
128
+ if (!branch) {
129
+ const made = await makeSolutionBranch(client, solutionId, recorded, cwd, outcome);
130
+ return { branch: made, notice: checkOut(cwd, made) };
131
+ }
132
+ // Claimed, and nothing pushed to it yet: another first session claimed it
133
+ // a moment ago and may still be making it (SOLBRANCH-1.3). Whatever the
134
+ // repository has by that name now is the solution's, by its record.
135
+ if (recorded.commits?.length === 1) {
136
+ branchHead(cwd, branch, recorded.commits[0]);
137
+ }
138
+ return { branch, notice: checkOut(cwd, branch) };
139
+ }
140
+ /**
141
+ * Make the solution's branch from the default branch as it stands now, or
142
+ * from `baseBranch` when a rehearsal's scenario named one (REHEARSE-4.5),
143
+ * named for its directory, suffixed past any name a branch already has
144
+ * (SOLBRANCH-1.0, 1.1). The name is claimed on the solution before the
145
+ * branch is made, so two first sessions arriving together are given the
146
+ * one name and the one commit, and both work on the one branch (1.3):
147
+ * whichever pushes second finds it already made, by the solution.
148
+ */
149
+ async function makeSolutionBranch(client, solutionId, recorded, cwd, outcome) {
150
+ const { directory } = recorded;
151
+ const claim = (args) => client.mutation(api.solutions.branches.claim, {
152
+ solutionId,
153
+ ...(outcome ? { outcome } : {}),
154
+ ...args,
155
+ });
156
+ const base = recorded.baseBranch
157
+ ? branchCommit(cwd, recorded.baseBranch)
158
+ : defaultBranchCommit(cwd);
159
+ let claimed = await claim({
160
+ name: freeName(directory, remoteBranches(cwd)),
161
+ base,
162
+ });
163
+ for (let attempt = 0; attempt < 5; attempt++) {
164
+ // The solution's when it is at a commit its agents left it at: made
165
+ // now, or by the other first session that claimed it.
166
+ const head = branchHead(cwd, claimed.name, claimed.base);
167
+ if (claimed.commits.includes(head))
168
+ return claimed.name;
169
+ // A branch the solution didn't make took the name after it was found
170
+ // free: the claim moves to the next name no branch has (1.1).
171
+ const taken = remoteBranches(cwd).add(claimed.name);
172
+ claimed = await claim({
173
+ name: freeName(directory, taken),
174
+ base: claimed.base,
175
+ replacing: claimed.name,
176
+ });
177
+ }
178
+ throw new Error(`Couldn't make a branch for the solution: other branches named dotreq/${directory} kept appearing.`);
179
+ }
180
+ function listed(names) {
181
+ return names.length <= 1
182
+ ? (names[0] ?? "")
183
+ : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
184
+ }
185
+ //# sourceMappingURL=pushing.js.map