@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,202 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { api } from "../../convex.js";
4
+ import { currentBranch, headCommit, repositoryRoot } from "./branch.js";
5
+ import { solutionClient } from "./client.js";
6
+ import { readImage, uploadImage } from "./image.js";
7
+ import { screenshotPreview } from "./preview-screenshot.js";
8
+ import { checkPullRequest } from "./pull-request.js";
9
+ import { pushBeforeRecord, refuseUncommitted } from "./pushing.js";
10
+ import { onOutcome, requireWhere } from "./run-file.js";
11
+ import { summaryGiven } from "./summary.js";
12
+ import { parseThing, REFERENCE_FORMS } from "./things.js";
13
+ import { readDemoScript, readRecording, submittedRecording, } from "./video.js";
14
+ import { readSubmittedFiles, recordSavedIds, } from "./working-folder.js";
15
+ /**
16
+ * `dotreq solution author <reference>... --comment "<what changed and why>"`:
17
+ * the drafter's submission of the outcome it is drafting, or of the one
18
+ * `--outcome` names. The references say where what it made is; the comment
19
+ * is what the next reader of the thread sees first, so nothing is recorded
20
+ * without it (VERB-1.2). On an outcome no agent reviews, the work goes to
21
+ * the owner with the executive summary, `--executive-summary "..."`, which
22
+ * the backend asks for there (VERB-1.6).
23
+ */
24
+ export async function authorCommand(references, options) {
25
+ if (references.length === 0) {
26
+ throw new Error("Name where what you made is: dotreq solution author <reference>... — `dotreq solution author --help` gives the forms.");
27
+ }
28
+ const comment = options.comment?.trim();
29
+ if (!comment) {
30
+ throw new Error("Say what changed and why: your comment is what the next reader sees first.");
31
+ }
32
+ const where = requireWhere(options);
33
+ const made = references.map(parseThing);
34
+ // SIGHT-12.3, 12.3.0: each reference's title, matched to it by order
35
+ // before anything is read, pushed, uploaded or recorded.
36
+ const titles = titleReferences(made, options.title ?? []);
37
+ // SIGHT-7.0.0, 7.0.1, 9.0.0, 13.0: a video and its script, a demo's
38
+ // script, and an image, are read first, so one a browser can't show, or
39
+ // a script that isn't one, is refused before anything is pushed,
40
+ // uploaded or recorded.
41
+ const videos = new Map();
42
+ const demoScripts = new Map();
43
+ const images = new Map();
44
+ for (const thing of made) {
45
+ if (thing.kind === "video") {
46
+ videos.set(thing, readRecording(thing.path, thing.scriptPath));
47
+ }
48
+ if (thing.kind === "demo script") {
49
+ demoScripts.set(thing, readDemoScript(thing.path));
50
+ }
51
+ if (thing.kind === "image")
52
+ images.set(thing, readImage(thing.path));
53
+ }
54
+ // VERB-1.4, 1.5: a file named without its branch is on the branch the
55
+ // checkout is on, or on none.
56
+ const cwd = process.cwd();
57
+ refuseOutsideCheckout(made, cwd);
58
+ const checkout = currentBranch(cwd);
59
+ const fromCheckout = (thing) => thing.kind === "file" &&
60
+ (thing.branch === undefined || thing.branch === checkout);
61
+ // PULL-3.0, 3.2: the pull request named is the solution's, open from its
62
+ // branch, checked before anything is pushed or recorded.
63
+ const pullRequests = new Map();
64
+ for (const thing of made) {
65
+ if (thing.kind === "pull request") {
66
+ pullRequests.set(thing, await checkPullRequest(where, thing.number));
67
+ }
68
+ }
69
+ const client = solutionClient();
70
+ if (where.branch) {
71
+ // Inside a runner: only what is committed is submitted (SOLBRANCH-5),
72
+ // and it reaches the solution's branch before the record is kept (3.0).
73
+ refuseUncommitted({ ...where, branch: where.branch }, made.flatMap((thing) => fromCheckout(thing) && thing.kind === "file" ? [thing.path] : []));
74
+ await pushBeforeRecord(client, where);
75
+ }
76
+ // SOLDOC-12.2.0: a file from the checkout's own branch keeps the commit
77
+ // it was submitted at, so the page can tell when it changes after.
78
+ const commit = checkout ? headCommit(cwd) : null;
79
+ /** A reference as it is submitted: where the thing is, as the backend keeps it. */
80
+ const submissionOf = async (thing) => {
81
+ const recording = videos.get(thing);
82
+ if (recording) {
83
+ // SIGHT-7.0, 7.4: the app keeps a copy of the video, the script's
84
+ // text and where each of its steps begins; nothing goes on the
85
+ // branch.
86
+ return submittedRecording(client, where.solutionId, recording);
87
+ }
88
+ const demoScript = demoScripts.get(thing);
89
+ if (demoScript) {
90
+ // SIGHT-13.0: the app keeps the script's text for the agent
91
+ // reviewing it to record; nothing goes on the branch.
92
+ return { kind: "demo script", ...demoScript };
93
+ }
94
+ const image = images.get(thing);
95
+ if (image) {
96
+ // SIGHT-9.0: the app keeps a copy of the image; nothing goes on the
97
+ // branch.
98
+ const storageId = await uploadImage(client, where.solutionId, image);
99
+ return { kind: "image", storageId, name: image.name };
100
+ }
101
+ const pullRequest = pullRequests.get(thing);
102
+ if (pullRequest)
103
+ return { kind: "pull request", ...pullRequest };
104
+ if (thing.kind === "preview") {
105
+ // SIGHT-12.0, 12.2: a screenshot of the page it opens on, kept as an
106
+ // image is; without one, it is submitted all the same.
107
+ const shot = await screenshotPreview(thing);
108
+ if ("image" in shot) {
109
+ const screenshot = await uploadImage(client, where.solutionId, shot.image);
110
+ return { ...thing, screenshot };
111
+ }
112
+ console.error(`No screenshot of preview:${thing.port}${thing.page ?? ""} was taken (${shot.missed}); its tab says so. Submit it again once it answers to add one.`);
113
+ return thing;
114
+ }
115
+ return checkout && fromCheckout(thing)
116
+ ? { ...thing, branch: checkout, ...(commit ? { commit } : {}) }
117
+ : thing;
118
+ };
119
+ const submitted = [];
120
+ for (const thing of made) {
121
+ // SIGHT-12.3: the title given for it goes with it, whatever its kind.
122
+ const title = titles.get(thing);
123
+ submitted.push({
124
+ ...(await submissionOf(thing)),
125
+ ...(title ? { title } : {}),
126
+ });
127
+ }
128
+ // A document submitted by its file goes with its text when there is no
129
+ // git checkout: on a project with no repository connected, the backend
130
+ // saves it on the solution's branch (BRANCH-1.0).
131
+ const files = checkout ? [] : readSubmittedFiles(made);
132
+ // VERB-1.6: on an outcome no agent reviews, the owner reads the executive
133
+ // summary first. Only the backend knows whether one does, so it is the
134
+ // backend that refuses the work without one.
135
+ const summary = summaryGiven(options);
136
+ const args = {
137
+ solutionId: where.solutionId,
138
+ agent: where.agent,
139
+ ...(where.outcome ? { outcome: where.outcome } : {}),
140
+ made: submitted,
141
+ comment,
142
+ ...(summary ? { summary } : {}),
143
+ ...(files.length ? { files } : {}),
144
+ };
145
+ const result = await client.mutation(api.solutions.mutations.complete, args);
146
+ const saved = result?.saved ?? [];
147
+ recordSavedIds(files, saved);
148
+ for (const entry of saved) {
149
+ console.log(`Saved ${entry.path} on the solution's branch as version ${entry.version}.`);
150
+ }
151
+ // CHECKPOINT-6.3.2.3: work begun before the outcome was set to be
152
+ // drafted again is kept, but isn't its new draft; the agent is told so.
153
+ if (result?.setAside) {
154
+ console.log(result.setAside);
155
+ return;
156
+ }
157
+ console.log(`Recorded your work${onOutcome(where.outcome)}.`);
158
+ }
159
+ /**
160
+ * SOLBRANCH-5.3: a file is submitted by where it is in the repository, so
161
+ * one from outside the checkout, such as a video written under /tmp, would
162
+ * be recorded at a path nobody can open. Refused, with the other forms, so
163
+ * the agent finds the one it meant.
164
+ */
165
+ function refuseOutsideCheckout(made, cwd) {
166
+ const root = repositoryRoot(cwd);
167
+ if (!root)
168
+ return;
169
+ const top = realpathSync(root);
170
+ const here = realpathSync(cwd);
171
+ const outside = made.flatMap((thing) => {
172
+ if (thing.kind !== "file")
173
+ return [];
174
+ const within = relative(top, resolve(here, thing.path));
175
+ return within.startsWith("..") || isAbsolute(within) ? [thing.path] : [];
176
+ });
177
+ if (outside.length === 0)
178
+ return;
179
+ throw new Error(`Nothing was recorded: ${outside.join(", ")} ${outside.length === 1 ? "is" : "are"} outside your checkout, and only files in the repository are submitted as files. ${REFERENCE_FORMS}`);
180
+ }
181
+ /**
182
+ * SIGHT-12.3, 12.3.0, 12.3.1: the title each reference is submitted with,
183
+ * its name on the outcome's tab, given as one `--title` per reference in the
184
+ * order the references are named, as `git commit -m` takes one per
185
+ * paragraph. None given, each keeps the name the tab gives it; a count that
186
+ * doesn't match submits nothing. A blank title is none, for that reference
187
+ * alone.
188
+ */
189
+ function titleReferences(made, given) {
190
+ if (given.length > 0 && given.length !== made.length) {
191
+ const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
192
+ throw new Error(`You named ${count(made.length, "reference", "references")} and gave ${count(given.length, "--title", "--titles")}, so nothing was submitted. Give one --title per reference, in the order you name them, or none.`);
193
+ }
194
+ const titles = new Map();
195
+ given.forEach((text, at) => {
196
+ const title = text.trim();
197
+ if (title)
198
+ titles.set(made[at], title);
199
+ });
200
+ return titles;
201
+ }
202
+ //# sourceMappingURL=author.js.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The git a solution's branch needs (SOLBRANCH): which branch a checkout
3
+ * is on, what it has not committed, bringing the branch's new commits in
4
+ * under the agent's own, and pushing. Every function runs git in the
5
+ * directory it is given and never commits: what an agent has not committed
6
+ * stays uncommitted, in its checkout (SOLBRANCH-6).
7
+ */
8
+ type Ran = {
9
+ code: number;
10
+ stdout: string;
11
+ stderr: string;
12
+ };
13
+ export declare function git(cwd: string, args: string[]): Ran;
14
+ /** Git's name for no commit: a push's `before` when the branch wasn't in the repository. */
15
+ export declare const NO_COMMIT = "0000000000000000000000000000000000000000";
16
+ /**
17
+ * The branch the checkout is on, or null when it is on no branch — a
18
+ * single commit checked out, or no git repository at all (VERB-1.4, 1.5).
19
+ */
20
+ export declare function currentBranch(cwd: string): string | null;
21
+ /** The top of the repository the checkout is in, or null outside one. */
22
+ export declare function repositoryRoot(cwd: string): string | null;
23
+ /** The commit the checkout is at, or null outside a git repository. */
24
+ export declare function headCommit(cwd: string): string | null;
25
+ /**
26
+ * What the checkout has not committed: files git tracks with changes,
27
+ * staged or not, and new files it does not ignore, one by one.
28
+ */
29
+ export declare function uncommitted(cwd: string): {
30
+ changed: string[];
31
+ untracked: string[];
32
+ };
33
+ export type BroughtIn = {
34
+ ok: true;
35
+ } | {
36
+ ok: false;
37
+ conflicts: string[];
38
+ } | {
39
+ ok: false;
40
+ problem: string;
41
+ };
42
+ /**
43
+ * Bring the branch's new commits into the checkout, the agent's own
44
+ * commits placed after them (SOLBRANCH-2, 3.1). When they conflict — with
45
+ * a commit of the agent's, or with a change it has not committed — the
46
+ * checkout is left exactly as it was, and the files that conflict are
47
+ * named (SOLBRANCH-2.3, 3.2). What it has not committed stays uncommitted.
48
+ */
49
+ export declare function bringIn(cwd: string, branch: string): BroughtIn;
50
+ export type Pushed = {
51
+ kind: "pushed";
52
+ before: string;
53
+ after: string;
54
+ } | {
55
+ kind: "nothing to push";
56
+ } | {
57
+ kind: "conflict";
58
+ files: string[];
59
+ } | {
60
+ kind: "refused";
61
+ reason: string;
62
+ };
63
+ /**
64
+ * Push the checkout's commits to the solution's branch, after bringing in
65
+ * what other agents pushed since (SOLBRANCH-3.1). A push that loses a race
66
+ * with another agent's tries again; one the repository refuses comes back
67
+ * with the reason it gave (3.3).
68
+ */
69
+ export declare function pushTo(cwd: string, branch: string): Pushed;
70
+ /** The names of the repository's branches, as it has them now. */
71
+ export declare function remoteBranches(cwd: string): Set<string>;
72
+ /**
73
+ * The name for a solution's new branch: `dotreq/<directory>`, or, when a
74
+ * branch already has that name, the lowest suffix from -2 up that none has
75
+ * (SOLBRANCH-1.0, 1.1).
76
+ */
77
+ export declare function freeName(directory: string, taken: Set<string>): string;
78
+ /** The default branch's commit, as the repository has it now, fetched into the checkout. */
79
+ export declare function defaultBranchCommit(cwd: string): string;
80
+ /**
81
+ * The commit a named branch is at, as the repository has it now, fetched
82
+ * into the checkout: what a solution's branch is made from when a
83
+ * rehearsal's scenario names the branch to start from (REHEARSE-4.5).
84
+ */
85
+ export declare function branchCommit(cwd: string, branch: string): string;
86
+ /**
87
+ * The commit the repository's branch `name` is at, making the branch at
88
+ * `base` first when the repository has none of that name (SOLBRANCH-1.0,
89
+ * 1.3). A branch of that name that appears first — another of the
90
+ * solution's first sessions making it a moment before, or anyone's — is
91
+ * never moved: its commit is read and returned, for the caller to judge
92
+ * whose it is.
93
+ */
94
+ export declare function branchHead(cwd: string, name: string, base: string): string;
95
+ /**
96
+ * Put the checkout on the solution's branch, and bring in what other steps
97
+ * pushed to it (SOLBRANCH-1.2, 2). Returns what the agent should be told,
98
+ * or null when there is nothing to tell: its checkout is on the branch and
99
+ * holds everything pushed to it.
100
+ */
101
+ export declare function checkOut(cwd: string, branch: string): string | null;
102
+ export {};
103
+ //# sourceMappingURL=branch.d.ts.map
@@ -0,0 +1,341 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export function git(cwd, args) {
3
+ const ran = spawnSync("git", args, {
4
+ cwd,
5
+ encoding: "utf8",
6
+ // A push that wants a password fails rather than waiting for one.
7
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
8
+ });
9
+ return {
10
+ code: ran.status ?? 1,
11
+ stdout: ran.stdout ?? "",
12
+ stderr: ran.stderr ?? "",
13
+ };
14
+ }
15
+ /** Git's name for no commit: a push's `before` when the branch wasn't in the repository. */
16
+ export const NO_COMMIT = "0000000000000000000000000000000000000000";
17
+ /**
18
+ * The branch the checkout is on, or null when it is on no branch — a
19
+ * single commit checked out, or no git repository at all (VERB-1.4, 1.5).
20
+ */
21
+ export function currentBranch(cwd) {
22
+ const ran = git(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
23
+ return ran.code === 0 ? ran.stdout.trim() || null : null;
24
+ }
25
+ /** The top of the repository the checkout is in, or null outside one. */
26
+ export function repositoryRoot(cwd) {
27
+ const ran = git(cwd, ["rev-parse", "--show-toplevel"]);
28
+ return ran.code === 0 ? ran.stdout.trim() || null : null;
29
+ }
30
+ /** The commit the checkout is at, or null outside a git repository. */
31
+ export function headCommit(cwd) {
32
+ const ran = git(cwd, ["rev-parse", "--verify", "--quiet", "HEAD"]);
33
+ return ran.code === 0 ? ran.stdout.trim() || null : null;
34
+ }
35
+ function commitOf(cwd, ref) {
36
+ const ran = git(cwd, ["rev-parse", "--verify", "--quiet", ref]);
37
+ return ran.code === 0 ? ran.stdout.trim() || null : null;
38
+ }
39
+ function isAncestor(cwd, a, b) {
40
+ return git(cwd, ["merge-base", "--is-ancestor", a, b]).code === 0;
41
+ }
42
+ /**
43
+ * What the checkout has not committed: files git tracks with changes,
44
+ * staged or not, and new files it does not ignore, one by one.
45
+ */
46
+ export function uncommitted(cwd) {
47
+ const ran = git(cwd, [
48
+ "status",
49
+ "--porcelain=v1",
50
+ "-z",
51
+ "--untracked-files=all",
52
+ ]);
53
+ const changed = [];
54
+ const untracked = [];
55
+ const entries = ran.stdout.split("\0");
56
+ for (let i = 0; i < entries.length; i++) {
57
+ const entry = entries[i];
58
+ if (entry.length < 4)
59
+ continue;
60
+ const status = entry.slice(0, 2);
61
+ const path = entry.slice(3);
62
+ if (status === "??") {
63
+ untracked.push(path);
64
+ continue;
65
+ }
66
+ changed.push(path);
67
+ // A rename or copy names where it came from next; that path changed too.
68
+ if (status[0] === "R" || status[0] === "C") {
69
+ const from = entries[++i];
70
+ if (from)
71
+ changed.push(from);
72
+ }
73
+ }
74
+ return { changed, untracked };
75
+ }
76
+ /** The remote-tracking ref a solution's branch is fetched into. */
77
+ function upstreamOf(branch) {
78
+ return `refs/remotes/origin/${branch}`;
79
+ }
80
+ /** Fetch the branch as the repository has it now; false when the repository has no such branch. */
81
+ function fetchBranch(cwd, branch) {
82
+ const ran = git(cwd, [
83
+ "fetch",
84
+ "--quiet",
85
+ "origin",
86
+ `+refs/heads/${branch}:${upstreamOf(branch)}`,
87
+ ]);
88
+ if (ran.code === 0)
89
+ return { ok: true, there: true };
90
+ if (/couldn't find remote ref/i.test(ran.stderr)) {
91
+ // Gone from the repository: so is what the checkout remembered of it.
92
+ git(cwd, ["update-ref", "-d", upstreamOf(branch)]);
93
+ return { ok: true, there: false };
94
+ }
95
+ return { ok: false, problem: lastLines(ran.stderr || ran.stdout) };
96
+ }
97
+ /**
98
+ * Bring the branch's new commits into the checkout, the agent's own
99
+ * commits placed after them (SOLBRANCH-2, 3.1). When they conflict — with
100
+ * a commit of the agent's, or with a change it has not committed — the
101
+ * checkout is left exactly as it was, and the files that conflict are
102
+ * named (SOLBRANCH-2.3, 3.2). What it has not committed stays uncommitted.
103
+ */
104
+ export function bringIn(cwd, branch) {
105
+ const fetched = fetchBranch(cwd, branch);
106
+ if (!fetched.ok)
107
+ return fetched;
108
+ if (!fetched.there)
109
+ return { ok: true };
110
+ const upstream = upstreamOf(branch);
111
+ if (isAncestor(cwd, upstream, "HEAD"))
112
+ return { ok: true };
113
+ // A change not committed can't be carried over a commit that touches the
114
+ // same file without conflicting, so that is a conflict before anything
115
+ // moves; nor can a new file the branch now adds.
116
+ const incoming = new Set(git(cwd, ["diff", "--name-only", "-z", `HEAD...${upstream}`])
117
+ .stdout.split("\0")
118
+ .filter(Boolean));
119
+ const { changed, untracked } = uncommitted(cwd);
120
+ const clashing = [...changed, ...untracked].filter((path) => incoming.has(path));
121
+ if (clashing.length > 0)
122
+ return { ok: false, conflicts: clashing.sort() };
123
+ if (isAncestor(cwd, "HEAD", upstream)) {
124
+ const merged = git(cwd, ["merge", "--ff-only", "--quiet", upstream]);
125
+ return merged.code === 0
126
+ ? { ok: true }
127
+ : { ok: false, problem: lastLines(merged.stderr || merged.stdout) };
128
+ }
129
+ const rebased = git(cwd, ["rebase", "--autostash", "--quiet", upstream]);
130
+ if (rebased.code === 0)
131
+ return { ok: true };
132
+ const conflicts = git(cwd, ["diff", "--name-only", "--diff-filter=U", "-z"])
133
+ .stdout.split("\0")
134
+ .filter(Boolean)
135
+ .sort();
136
+ // Undoing the rebase puts back the agent's commits and, from the
137
+ // autostash, what it had not committed.
138
+ git(cwd, ["rebase", "--abort"]);
139
+ return conflicts.length > 0
140
+ ? { ok: false, conflicts }
141
+ : { ok: false, problem: lastLines(rebased.stderr || rebased.stdout) };
142
+ }
143
+ /**
144
+ * Push the checkout's commits to the solution's branch, after bringing in
145
+ * what other agents pushed since (SOLBRANCH-3.1). A push that loses a race
146
+ * with another agent's tries again; one the repository refuses comes back
147
+ * with the reason it gave (3.3).
148
+ */
149
+ export function pushTo(cwd, branch) {
150
+ for (let attempt = 0; attempt < 3; attempt++) {
151
+ const brought = bringIn(cwd, branch);
152
+ if (!brought.ok) {
153
+ return "conflicts" in brought
154
+ ? { kind: "conflict", files: brought.conflicts }
155
+ : { kind: "refused", reason: brought.problem };
156
+ }
157
+ const before = commitOf(cwd, upstreamOf(branch));
158
+ const after = headCommit(cwd);
159
+ if (!after)
160
+ return { kind: "nothing to push" };
161
+ if (before && isAncestor(cwd, "HEAD", before)) {
162
+ return { kind: "nothing to push" };
163
+ }
164
+ const pushed = git(cwd, [
165
+ "push",
166
+ "--porcelain",
167
+ "origin",
168
+ `HEAD:refs/heads/${branch}`,
169
+ ]);
170
+ if (pushed.code === 0) {
171
+ return { kind: "pushed", before: before ?? NO_COMMIT, after };
172
+ }
173
+ if (/\[rejected\].*\((fetch first|non-fast-forward)\)/.test(pushed.stdout)) {
174
+ continue;
175
+ }
176
+ return { kind: "refused", reason: refusalOf(pushed) };
177
+ }
178
+ return {
179
+ kind: "refused",
180
+ reason: `other agents kept pushing to ${branch} while this one tried to`,
181
+ };
182
+ }
183
+ /** What the repository said when it refused a push: its own lines, else git's reason. */
184
+ function refusalOf(ran) {
185
+ const said = ran.stderr
186
+ .split("\n")
187
+ .filter((line) => line.startsWith("remote:"))
188
+ .map((line) => line
189
+ .replace(/^remote:\s*/, "")
190
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the escape starts colour codes a remote sends
191
+ .replace(/\u001b\[[0-9;]*m/g, "")
192
+ .trim())
193
+ .filter(Boolean);
194
+ if (said.length > 0)
195
+ return said.join(" ");
196
+ const reason = ran.stdout.match(/\[(?:remote )?rejected\]\s*\(([^)]+)\)/);
197
+ return reason ? reason[1] : lastLines(ran.stderr || ran.stdout);
198
+ }
199
+ function lastLines(text) {
200
+ return text.trim().split("\n").slice(-3).join(" ").trim();
201
+ }
202
+ /** The names of the repository's branches, as it has them now. */
203
+ export function remoteBranches(cwd) {
204
+ const ran = git(cwd, ["ls-remote", "--heads", "origin"]);
205
+ if (ran.code !== 0) {
206
+ throw new Error(`Couldn't list the repository's branches: ${lastLines(ran.stderr)}`);
207
+ }
208
+ return new Set(ran.stdout
209
+ .split("\n")
210
+ .map((line) => line.split("\t")[1]?.replace(/^refs\/heads\//, ""))
211
+ .filter((name) => !!name));
212
+ }
213
+ /**
214
+ * The name for a solution's new branch: `dotreq/<directory>`, or, when a
215
+ * branch already has that name, the lowest suffix from -2 up that none has
216
+ * (SOLBRANCH-1.0, 1.1).
217
+ */
218
+ export function freeName(directory, taken) {
219
+ const base = `dotreq/${directory}`;
220
+ if (!taken.has(base))
221
+ return base;
222
+ for (let n = 2;; n++) {
223
+ if (!taken.has(`${base}-${n}`))
224
+ return `${base}-${n}`;
225
+ }
226
+ }
227
+ /** The default branch's commit, as the repository has it now, fetched into the checkout. */
228
+ export function defaultBranchCommit(cwd) {
229
+ const fetched = git(cwd, ["fetch", "--quiet", "origin", "HEAD"]);
230
+ const commit = fetched.code === 0 ? commitOf(cwd, "FETCH_HEAD") : null;
231
+ if (!commit) {
232
+ throw new Error(`Couldn't read the repository's default branch: ${lastLines(fetched.stderr)}`);
233
+ }
234
+ return commit;
235
+ }
236
+ /**
237
+ * The commit a named branch is at, as the repository has it now, fetched
238
+ * into the checkout: what a solution's branch is made from when a
239
+ * rehearsal's scenario names the branch to start from (REHEARSE-4.5).
240
+ */
241
+ export function branchCommit(cwd, branch) {
242
+ const fetched = fetchBranch(cwd, branch);
243
+ if (!fetched.ok) {
244
+ throw new Error(`Couldn't fetch ${branch}: ${fetched.problem}`);
245
+ }
246
+ const commit = fetched.there ? commitOf(cwd, upstreamOf(branch)) : null;
247
+ if (!commit) {
248
+ throw new Error(`The branch ${branch}, which the solution's branch is to be made from, isn't in the repository.`);
249
+ }
250
+ return commit;
251
+ }
252
+ /**
253
+ * The commit the repository's branch `name` is at, making the branch at
254
+ * `base` first when the repository has none of that name (SOLBRANCH-1.0,
255
+ * 1.3). A branch of that name that appears first — another of the
256
+ * solution's first sessions making it a moment before, or anyone's — is
257
+ * never moved: its commit is read and returned, for the caller to judge
258
+ * whose it is.
259
+ */
260
+ export function branchHead(cwd, name, base) {
261
+ for (let attempt = 0; attempt < 3; attempt++) {
262
+ const fetched = fetchBranch(cwd, name);
263
+ if (!fetched.ok) {
264
+ throw new Error(`Couldn't fetch ${name}: ${fetched.problem}`);
265
+ }
266
+ if (fetched.there) {
267
+ const head = commitOf(cwd, upstreamOf(name));
268
+ if (head)
269
+ return head;
270
+ }
271
+ // Another session may have fetched a newer default branch and claimed
272
+ // first; its commit is the one to make the branch from.
273
+ if (commitOf(cwd, `${base}^{commit}`) === null) {
274
+ const got = git(cwd, ["fetch", "--quiet", "origin", base]);
275
+ if (got.code !== 0) {
276
+ throw new Error(`Couldn't fetch ${base}, which the solution's branch is made from: ${lastLines(got.stderr)}`);
277
+ }
278
+ }
279
+ // Made only if no branch has the name; one that got there first is
280
+ // read on the next pass.
281
+ const pushed = git(cwd, [
282
+ "push",
283
+ "--porcelain",
284
+ `--force-with-lease=refs/heads/${name}:`,
285
+ "origin",
286
+ `${base}:refs/heads/${name}`,
287
+ ]);
288
+ if (pushed.code === 0)
289
+ return base;
290
+ if (!/\[rejected\]|already exists/.test(pushed.stdout + pushed.stderr)) {
291
+ throw new Error(`The repository refused to make the branch ${name}: ${refusalOf(pushed)}`);
292
+ }
293
+ }
294
+ throw new Error(`Couldn't make the branch ${name}: it kept changing while this session read it.`);
295
+ }
296
+ /**
297
+ * Put the checkout on the solution's branch, and bring in what other steps
298
+ * pushed to it (SOLBRANCH-1.2, 2). Returns what the agent should be told,
299
+ * or null when there is nothing to tell: its checkout is on the branch and
300
+ * holds everything pushed to it.
301
+ */
302
+ export function checkOut(cwd, branch) {
303
+ const fetched = fetchBranch(cwd, branch);
304
+ if (!fetched.ok) {
305
+ throw new Error(`Couldn't fetch ${branch}: ${fetched.problem}`);
306
+ }
307
+ if (!fetched.there) {
308
+ throw new Error(`The solution's branch ${branch} is no longer in the repository.`);
309
+ }
310
+ const current = currentBranch(cwd);
311
+ if (current !== branch) {
312
+ const local = commitOf(cwd, `refs/heads/${branch}`) !== null;
313
+ const switched = local
314
+ ? git(cwd, ["checkout", "--quiet", branch])
315
+ : git(cwd, [
316
+ "checkout",
317
+ "--quiet",
318
+ "-b",
319
+ branch,
320
+ "--no-track",
321
+ upstreamOf(branch),
322
+ ]);
323
+ if (switched.code !== 0) {
324
+ return [
325
+ `Your checkout is on ${current ?? "no branch"}, not the solution's branch ${branch}, and switching to it would overwrite changes you haven't committed: ${lastLines(switched.stderr)}`,
326
+ `Switch to ${branch} before you record anything: what you record is pushed to ${branch}.`,
327
+ ].join("\n");
328
+ }
329
+ }
330
+ const brought = bringIn(cwd, branch);
331
+ if (brought.ok)
332
+ return null;
333
+ if ("problem" in brought) {
334
+ throw new Error(`Couldn't bring in ${branch}'s new commits: ${brought.problem}`);
335
+ }
336
+ return [
337
+ `Other agents pushed commits to ${branch} that conflict with work you left in your checkout, in ${brought.conflicts.join(", ")}.`,
338
+ `Your checkout is as you left it. Bring the branch's commits in before you record anything (git pull --rebase --autostash origin ${branch}), and resolve the conflicts.`,
339
+ ].join("\n");
340
+ }
341
+ //# sourceMappingURL=branch.js.map