@popoverai/dotrequirements 0.32.1 → 0.34.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 (135) hide show
  1. package/dist/cli.js +32 -7
  2. package/dist/cloud.d.ts +18 -0
  3. package/dist/cloud.js +38 -0
  4. package/dist/commands/board/index.d.ts +13 -0
  5. package/dist/commands/board/index.js +96 -0
  6. package/dist/commands/branch-merge.d.ts +78 -0
  7. package/dist/commands/branch-merge.js +129 -0
  8. package/dist/commands/create-requirement-document.js +2 -2
  9. package/dist/commands/diff.js +15 -5
  10. package/dist/commands/init.js +7 -6
  11. package/dist/commands/link.js +27 -21
  12. package/dist/commands/mockup/index.d.ts +11 -0
  13. package/dist/commands/mockup/index.js +82 -0
  14. package/dist/commands/report.js +22 -9
  15. package/dist/commands/review-test.js +3 -2
  16. package/dist/commands/scores/index.d.ts +50 -0
  17. package/dist/commands/scores/index.js +190 -0
  18. package/dist/commands/solution/ask.d.ts +9 -0
  19. package/dist/commands/solution/ask.js +27 -0
  20. package/dist/commands/solution/author.d.ts +19 -0
  21. package/dist/commands/solution/author.js +202 -0
  22. package/dist/commands/solution/branch.d.ts +103 -0
  23. package/dist/commands/solution/branch.js +341 -0
  24. package/dist/commands/solution/brief.d.ts +81 -0
  25. package/dist/commands/solution/brief.js +47 -0
  26. package/dist/commands/solution/checkout.d.ts +27 -0
  27. package/dist/commands/solution/checkout.js +66 -0
  28. package/dist/commands/solution/client.d.ts +16 -0
  29. package/dist/commands/solution/client.js +46 -0
  30. package/dist/commands/solution/contract.d.ts +9 -0
  31. package/dist/commands/solution/contract.js +23 -0
  32. package/dist/commands/solution/create.d.ts +14 -0
  33. package/dist/commands/solution/create.js +52 -0
  34. package/dist/commands/solution/demo-script.d.ts +28 -0
  35. package/dist/commands/solution/demo-script.js +43 -0
  36. package/dist/commands/solution/image.d.ts +60 -0
  37. package/dist/commands/solution/image.js +148 -0
  38. package/dist/commands/solution/index.d.ts +12 -0
  39. package/dist/commands/solution/index.js +281 -0
  40. package/dist/commands/solution/look-up.d.ts +9 -0
  41. package/dist/commands/solution/look-up.js +69 -0
  42. package/dist/commands/solution/next.d.ts +12 -0
  43. package/dist/commands/solution/next.js +48 -0
  44. package/dist/commands/solution/plan-file.d.ts +22 -0
  45. package/dist/commands/solution/plan-file.js +33 -0
  46. package/dist/commands/solution/preview-screenshot.d.ts +21 -0
  47. package/dist/commands/solution/preview-screenshot.js +56 -0
  48. package/dist/commands/solution/pull-request.d.ts +25 -0
  49. package/dist/commands/solution/pull-request.js +117 -0
  50. package/dist/commands/solution/pushing.d.ts +66 -0
  51. package/dist/commands/solution/pushing.js +185 -0
  52. package/dist/commands/solution/rehearse.d.ts +10 -0
  53. package/dist/commands/solution/rehearse.js +24 -0
  54. package/dist/commands/solution/request.d.ts +3 -0
  55. package/dist/commands/solution/request.js +13 -0
  56. package/dist/commands/solution/review.d.ts +27 -0
  57. package/dist/commands/solution/review.js +104 -0
  58. package/dist/commands/solution/run-file.d.ts +42 -0
  59. package/dist/commands/solution/run-file.js +63 -0
  60. package/dist/commands/solution/share.d.ts +62 -0
  61. package/dist/commands/solution/share.js +346 -0
  62. package/dist/commands/solution/stall.d.ts +14 -0
  63. package/dist/commands/solution/stall.js +41 -0
  64. package/dist/commands/solution/start.d.ts +11 -0
  65. package/dist/commands/solution/start.js +20 -0
  66. package/dist/commands/solution/status.d.ts +39 -0
  67. package/dist/commands/solution/status.js +145 -0
  68. package/dist/commands/solution/summary.d.ts +13 -0
  69. package/dist/commands/solution/summary.js +5 -0
  70. package/dist/commands/solution/things.d.ts +118 -0
  71. package/dist/commands/solution/things.js +78 -0
  72. package/dist/commands/solution/thread.d.ts +13 -0
  73. package/dist/commands/solution/thread.js +53 -0
  74. package/dist/commands/solution/upload.d.ts +24 -0
  75. package/dist/commands/solution/upload.js +27 -0
  76. package/dist/commands/solution/video.d.ts +88 -0
  77. package/dist/commands/solution/video.js +364 -0
  78. package/dist/commands/solution/working-folder.d.ts +44 -0
  79. package/dist/commands/solution/working-folder.js +64 -0
  80. package/dist/commands/style-check.js +4 -4
  81. package/dist/commands/sync.d.ts +2 -1
  82. package/dist/commands/sync.js +21 -5
  83. package/dist/convex.d.ts +64 -10
  84. package/dist/convex.js +75 -10
  85. package/dist/harness/finalize.js +16 -8
  86. package/dist/harness/reportingStatus.js +2 -11
  87. package/dist/push/core.js +7 -2
  88. package/dist/rehearsal/derive.d.ts +23 -0
  89. package/dist/rehearsal/derive.js +112 -0
  90. package/dist/rehearsal/driver.d.ts +44 -0
  91. package/dist/rehearsal/driver.js +337 -0
  92. package/dist/rehearsal/earlier.d.ts +25 -0
  93. package/dist/rehearsal/earlier.js +138 -0
  94. package/dist/rehearsal/index.d.ts +5 -0
  95. package/dist/rehearsal/index.js +4 -0
  96. package/dist/rehearsal/live.d.ts +72 -0
  97. package/dist/rehearsal/live.js +301 -0
  98. package/dist/rehearsal/report.d.ts +4 -0
  99. package/dist/rehearsal/report.js +69 -0
  100. package/dist/rehearsal/types.d.ts +377 -0
  101. package/dist/rehearsal/types.js +8 -0
  102. package/dist/requirements/cloud-ai.d.ts +0 -6
  103. package/dist/requirements/cloud-ai.js +2 -11
  104. package/dist/requirements/cloud-coverage.js +4 -4
  105. package/dist/requirements/index.d.ts +6 -0
  106. package/dist/requirements/index.js +24 -0
  107. package/dist/schema/browser.d.ts +1 -1
  108. package/dist/schema/browser.js +3 -1
  109. package/dist/schema/file-writer.d.ts +5 -0
  110. package/dist/schema/file-writer.js +4 -0
  111. package/dist/schema/parser.d.ts +11 -2
  112. package/dist/schema/parser.js +11 -7
  113. package/dist/schema/schemas.d.ts +40 -0
  114. package/dist/schema/schemas.js +4 -0
  115. package/dist/sync/execute.d.ts +10 -1
  116. package/dist/sync/execute.js +76 -24
  117. package/dist/sync/index.d.ts +1 -1
  118. package/dist/sync/index.js +1 -1
  119. package/dist/sync/publish.d.ts +7 -0
  120. package/dist/sync/publish.js +2 -2
  121. package/dist/sync/segment.js +4 -1
  122. package/dist/sync/snapshot.d.ts +27 -5
  123. package/dist/sync/snapshot.js +92 -15
  124. package/dist/sync/solution-documents.d.ts +64 -0
  125. package/dist/sync/solution-documents.js +97 -0
  126. package/dist/sync/types.d.ts +19 -1
  127. package/dist/utils/command-error.d.ts +19 -0
  128. package/dist/utils/command-error.js +44 -0
  129. package/dist/utils/oauth-flow.js +2 -2
  130. package/dist/utils/project-selector.js +4 -4
  131. package/dist/version-messages.d.ts +28 -0
  132. package/dist/version-messages.js +87 -0
  133. package/package.json +3 -2
  134. package/dist/config.d.ts +0 -5
  135. package/dist/config.js +0 -12
@@ -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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `dotreq solution rehearse <scenario.yaml>` (REHEARSE-4): run a scenario
3
+ * against the linked project, real parties awaited, and print how it went.
4
+ * The scenario's earlier runs are removed first, unless `--keep` says to
5
+ * leave them for comparing (REHEARSE-6).
6
+ */
7
+ export declare function rehearseCommand(path: string, options?: {
8
+ keep?: boolean;
9
+ }): Promise<void>;
10
+ //# sourceMappingURL=rehearse.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { checkScenario, rehearse } from "../../rehearsal/driver.js";
2
+ import { removeEarlierRuns } from "../../rehearsal/earlier.js";
3
+ import { liveClient, scenarioFrom } from "../../rehearsal/live.js";
4
+ import { renderReport } from "../../rehearsal/report.js";
5
+ import { solutionClient } from "./client.js";
6
+ /**
7
+ * `dotreq solution rehearse <scenario.yaml>` (REHEARSE-4): run a scenario
8
+ * against the linked project, real parties awaited, and print how it went.
9
+ * The scenario's earlier runs are removed first, unless `--keep` says to
10
+ * leave them for comparing (REHEARSE-6).
11
+ */
12
+ export async function rehearseCommand(path, options = {}) {
13
+ const client = solutionClient();
14
+ const scenario = await scenarioFrom(path, client);
15
+ checkScenario(scenario);
16
+ const earlier = options.keep
17
+ ? { scenario: scenario.file ?? path, removed: 0, keep: true, kept: [] }
18
+ : await removeEarlierRuns(path, client);
19
+ const report = await rehearse(liveClient(), scenario);
20
+ process.stdout.write(renderReport({ ...report, earlier }));
21
+ if (!/^nothing ready$/.test(report.ended))
22
+ process.exitCode = 3;
23
+ }
24
+ //# sourceMappingURL=rehearse.js.map
@@ -0,0 +1,3 @@
1
+ /** A decision request from the JSON file an agent wrote. The backend validates it against the contract. */
2
+ export declare function readRequest(file: string): unknown;
3
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ /** A decision request from the JSON file an agent wrote. The backend validates it against the contract. */
4
+ export function readRequest(file) {
5
+ const text = readFileSync(resolve(process.cwd(), file), "utf8");
6
+ try {
7
+ return JSON.parse(text);
8
+ }
9
+ catch (error) {
10
+ throw new Error(`${file} isn't JSON: ${error instanceof Error ? error.message : String(error)}`);
11
+ }
12
+ }
13
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1,27 @@
1
+ import { type WhereOptions } from "./run-file.js";
2
+ import { type SummaryOptions } from "./summary.js";
3
+ export type ReviewOptions = WhereOptions & SummaryOptions & {
4
+ /** What should change, what was checked, or what the owner is to decide: required for every result (VERB-2.0, 2.1, 2.4). */
5
+ comment?: string;
6
+ /**
7
+ * With an approval of a demo's script: the video recorded from it and
8
+ * the script as the reviewer left it, each `"video:<video> <steps
9
+ * file>"`, given after the result on the command line (SIGHT-13.2).
10
+ */
11
+ videos?: string[];
12
+ };
13
+ /**
14
+ * `dotreq solution review <result> --comment "..."`: the reviewer's
15
+ * result on the outcome it is reviewing, or on the one `--outcome` names.
16
+ * The comment is what the next reader of the thread sees first — the
17
+ * drafter works from it, and on an escalation it is the note the owner
18
+ * reads — so every result is refused without one before anything is
19
+ * recorded (VERB-2.0, 2.1, 2.4). An approval also carries an executive
20
+ * summary, `--executive-summary "..."`, which is what the owner reads with
21
+ * the work (VERB-2.5); the outcome's own rules decide what follows. An
22
+ * approval of a demo's script carries the video the reviewer recorded from
23
+ * it, `"video:<video> <steps file>"`, which takes the script's place
24
+ * (SIGHT-13.2); no other result carries one (13.4.0).
25
+ */
26
+ export declare function reviewCommand(result: string, options: ReviewOptions): Promise<void>;
27
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { api } from "../../convex.js";
2
+ import { solutionClient } from "./client.js";
3
+ import { pushBeforeRecord } from "./pushing.js";
4
+ import { onOutcome, requireWhere } from "./run-file.js";
5
+ import { summaryGiven } from "./summary.js";
6
+ import { parseThing } from "./things.js";
7
+ import { readRecording, submittedRecording } from "./video.js";
8
+ /** What an agent types, and the result in the words the outcome keeps. */
9
+ const RESULTS = {
10
+ approved: "approved",
11
+ "approved-with-revisions": "approved with revisions",
12
+ "needs-revision": "needs revision",
13
+ escalate: "escalate",
14
+ };
15
+ /**
16
+ * `dotreq solution review <result> --comment "..."`: the reviewer's
17
+ * result on the outcome it is reviewing, or on the one `--outcome` names.
18
+ * The comment is what the next reader of the thread sees first — the
19
+ * drafter works from it, and on an escalation it is the note the owner
20
+ * reads — so every result is refused without one before anything is
21
+ * recorded (VERB-2.0, 2.1, 2.4). An approval also carries an executive
22
+ * summary, `--executive-summary "..."`, which is what the owner reads with
23
+ * the work (VERB-2.5); the outcome's own rules decide what follows. An
24
+ * approval of a demo's script carries the video the reviewer recorded from
25
+ * it, `"video:<video> <steps file>"`, which takes the script's place
26
+ * (SIGHT-13.2); no other result carries one (13.4.0).
27
+ */
28
+ export async function reviewCommand(result, options) {
29
+ const outcome = RESULTS[result];
30
+ if (!outcome) {
31
+ throw new Error(`A review result is approved, approved-with-revisions, needs-revision or escalate, not "${result}".`);
32
+ }
33
+ const comment = options.comment?.trim();
34
+ if (!comment) {
35
+ throw new Error(outcome === "approved"
36
+ ? "Say what you checked: your comment is what the next reader sees first."
37
+ : outcome === "escalate"
38
+ ? "Say what the owner is to decide: your comment is the note they read with the work."
39
+ : "Say what should change: the drafter works from your comment.");
40
+ }
41
+ // VERB-2.5, 2.5.0: an approval carries an executive summary for the
42
+ // owner, and nothing else does; refused before anything is recorded, in
43
+ // the backend's words.
44
+ const summary = summaryGiven(options);
45
+ const approving = outcome === "approved" || outcome === "approved with revisions";
46
+ if (approving && !summary) {
47
+ throw new Error("Write an executive summary for the owner with --executive-summary: it's what they read first. Say what this is and what most needs their eye, as the work will stand once any fixes you asked for are made.");
48
+ }
49
+ if (!approving && summary) {
50
+ throw new Error(`Only an approval carries an executive summary, so nothing was recorded. Leave --executive-summary off: ${outcome === "escalate" ? "your comment is the note the owner reads." : "the drafter works from your comment."}`);
51
+ }
52
+ // SIGHT-13.2, 13.4.0: a video goes only with a plain approval, and is
53
+ // read before anything is pushed, uploaded or recorded.
54
+ const recordings = readVideos(options.videos ?? []);
55
+ if (recordings.length > 0 && outcome !== "approved") {
56
+ throw new Error(outcome === "approved with revisions"
57
+ ? "Nothing was recorded: a demo's script isn't approved with revisions, because its drafter can't change the script without the video going out of step. Fix anything small yourself, record the video, and approve with it, saying in your comment what you changed; or give needs-revision."
58
+ : `Nothing was recorded: only an approval carries a video. Give your result without it: ${outcome === "escalate" ? "your comment is the note the owner reads" : "the drafter works from your comment"}, and no video is kept.`);
59
+ }
60
+ const where = requireWhere(options);
61
+ const { solutionId, agent } = where;
62
+ const client = solutionClient();
63
+ // SOLBRANCH-3.0: what the reviewer committed reaches the branch first.
64
+ await pushBeforeRecord(client, where);
65
+ const videos = [];
66
+ for (const recording of recordings) {
67
+ videos.push(await submittedRecording(client, solutionId, recording));
68
+ }
69
+ const args = {
70
+ solutionId,
71
+ agent,
72
+ ...(where.outcome ? { outcome: where.outcome } : {}),
73
+ review: {
74
+ outcome,
75
+ comment,
76
+ ...(summary ? { summary } : {}),
77
+ ...(videos.length ? { videos } : {}),
78
+ },
79
+ };
80
+ const said = await client.mutation(api.solutions.mutations.complete, args);
81
+ // STEP-REVIEW-18.7.0: the drafter submitted again after this review began.
82
+ // Nothing was recorded; the words say what changed and to give the result
83
+ // again once the reviewer has looked.
84
+ if (said?.refused)
85
+ throw new Error(said.refused);
86
+ console.log(outcome === "escalate"
87
+ ? `Escalated${onOutcome(where.outcome)}. It waits on the owner's decision.`
88
+ : `Recorded your review${onOutcome(where.outcome)}: ${outcome}.`);
89
+ }
90
+ /**
91
+ * The videos a review carries (SIGHT-13.2): each recorded from a demo's
92
+ * script, named as the drafter names one, `"video:<video> <steps file>"`.
93
+ * Nothing else goes with a review.
94
+ */
95
+ function readVideos(references) {
96
+ return references.map((reference) => {
97
+ const thing = parseThing(reference);
98
+ if (thing.kind !== "video") {
99
+ throw new Error(`Nothing was recorded: a review carries no "${reference}". The one thing it carries is the video you recorded from a demo's script, with your approval: "video:<video> <steps file>".`);
100
+ }
101
+ return readRecording(thing.path, thing.scriptPath);
102
+ });
103
+ }
104
+ //# sourceMappingURL=review.js.map