@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.
- package/README.md +2 -2
- package/dist/cli.js +13 -1
- package/dist/cloud.d.ts +5 -1
- package/dist/cloud.js +6 -2
- package/dist/commands/acceptance-test.js +1 -1
- package/dist/commands/board/index.d.ts +13 -0
- package/dist/commands/board/index.js +96 -0
- package/dist/commands/branch-merge.d.ts +78 -0
- package/dist/commands/branch-merge.js +129 -0
- package/dist/commands/diff.js +15 -5
- package/dist/commands/mockup/index.d.ts +11 -0
- package/dist/commands/mockup/index.js +82 -0
- package/dist/commands/scores/index.d.ts +50 -0
- package/dist/commands/scores/index.js +190 -0
- package/dist/commands/solution/ask.d.ts +9 -0
- package/dist/commands/solution/ask.js +27 -0
- package/dist/commands/solution/author.d.ts +19 -0
- package/dist/commands/solution/author.js +202 -0
- package/dist/commands/solution/branch.d.ts +103 -0
- package/dist/commands/solution/branch.js +341 -0
- package/dist/commands/solution/brief.d.ts +81 -0
- package/dist/commands/solution/brief.js +47 -0
- package/dist/commands/solution/checkout.d.ts +27 -0
- package/dist/commands/solution/checkout.js +66 -0
- package/dist/commands/solution/client.d.ts +16 -0
- package/dist/commands/solution/client.js +46 -0
- package/dist/commands/solution/contract.d.ts +9 -0
- package/dist/commands/solution/contract.js +23 -0
- package/dist/commands/solution/create.d.ts +14 -0
- package/dist/commands/solution/create.js +52 -0
- package/dist/commands/solution/demo-script.d.ts +28 -0
- package/dist/commands/solution/demo-script.js +43 -0
- package/dist/commands/solution/image.d.ts +60 -0
- package/dist/commands/solution/image.js +148 -0
- package/dist/commands/solution/index.d.ts +12 -0
- package/dist/commands/solution/index.js +282 -0
- package/dist/commands/solution/look-up.d.ts +9 -0
- package/dist/commands/solution/look-up.js +69 -0
- package/dist/commands/solution/next.d.ts +12 -0
- package/dist/commands/solution/next.js +48 -0
- package/dist/commands/solution/plan-file.d.ts +22 -0
- package/dist/commands/solution/plan-file.js +33 -0
- package/dist/commands/solution/preview-screenshot.d.ts +21 -0
- package/dist/commands/solution/preview-screenshot.js +56 -0
- package/dist/commands/solution/pull-request.d.ts +25 -0
- package/dist/commands/solution/pull-request.js +117 -0
- package/dist/commands/solution/pushing.d.ts +66 -0
- package/dist/commands/solution/pushing.js +185 -0
- package/dist/commands/solution/rehearse.d.ts +10 -0
- package/dist/commands/solution/rehearse.js +24 -0
- package/dist/commands/solution/request.d.ts +3 -0
- package/dist/commands/solution/request.js +13 -0
- package/dist/commands/solution/review.d.ts +27 -0
- package/dist/commands/solution/review.js +104 -0
- package/dist/commands/solution/run-file.d.ts +43 -0
- package/dist/commands/solution/run-file.js +69 -0
- package/dist/commands/solution/share.d.ts +62 -0
- package/dist/commands/solution/share.js +346 -0
- package/dist/commands/solution/stall.d.ts +14 -0
- package/dist/commands/solution/stall.js +41 -0
- package/dist/commands/solution/start.d.ts +14 -0
- package/dist/commands/solution/start.js +31 -0
- package/dist/commands/solution/status.d.ts +39 -0
- package/dist/commands/solution/status.js +145 -0
- package/dist/commands/solution/summary.d.ts +13 -0
- package/dist/commands/solution/summary.js +5 -0
- package/dist/commands/solution/things.d.ts +118 -0
- package/dist/commands/solution/things.js +78 -0
- package/dist/commands/solution/thread.d.ts +13 -0
- package/dist/commands/solution/thread.js +53 -0
- package/dist/commands/solution/upload.d.ts +24 -0
- package/dist/commands/solution/upload.js +27 -0
- package/dist/commands/solution/video.d.ts +88 -0
- package/dist/commands/solution/video.js +364 -0
- package/dist/commands/solution/working-folder.d.ts +44 -0
- package/dist/commands/solution/working-folder.js +64 -0
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +21 -5
- package/dist/convex.d.ts +54 -0
- package/dist/convex.js +64 -0
- package/dist/push/core.js +7 -2
- package/dist/rehearsal/derive.d.ts +23 -0
- package/dist/rehearsal/derive.js +112 -0
- package/dist/rehearsal/driver.d.ts +44 -0
- package/dist/rehearsal/driver.js +337 -0
- package/dist/rehearsal/earlier.d.ts +25 -0
- package/dist/rehearsal/earlier.js +138 -0
- package/dist/rehearsal/index.d.ts +5 -0
- package/dist/rehearsal/index.js +4 -0
- package/dist/rehearsal/live.d.ts +72 -0
- package/dist/rehearsal/live.js +301 -0
- package/dist/rehearsal/report.d.ts +4 -0
- package/dist/rehearsal/report.js +69 -0
- package/dist/rehearsal/types.d.ts +377 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/requirements/index.d.ts +6 -0
- package/dist/requirements/index.js +24 -0
- package/dist/schema/browser.d.ts +2 -1
- package/dist/schema/browser.js +6 -1
- package/dist/schema/file-writer.d.ts +5 -0
- package/dist/schema/file-writer.js +4 -0
- package/dist/schema/parser.d.ts +11 -2
- package/dist/schema/parser.js +11 -7
- package/dist/schema/provenance.d.ts +27 -0
- package/dist/schema/provenance.js +66 -0
- package/dist/schema/schemas.d.ts +40 -0
- package/dist/schema/schemas.js +4 -0
- package/dist/sync/execute.d.ts +10 -1
- package/dist/sync/execute.js +68 -5
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/publish.d.ts +7 -0
- package/dist/sync/segment.js +4 -1
- package/dist/sync/snapshot.d.ts +23 -1
- package/dist/sync/snapshot.js +75 -3
- package/dist/sync/solution-documents.d.ts +74 -0
- package/dist/sync/solution-documents.js +135 -0
- package/dist/sync/types.d.ts +20 -0
- 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
|