@popoverai/dotrequirements 0.33.0 → 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.
- package/dist/cli.js +13 -1
- package/dist/cloud.d.ts +5 -1
- package/dist/cloud.js +6 -2
- 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 +281 -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 +42 -0
- package/dist/commands/solution/run-file.js +63 -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 +11 -0
- package/dist/commands/solution/start.js +20 -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 +1 -1
- package/dist/schema/browser.js +3 -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/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 +69 -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 +64 -0
- package/dist/sync/solution-documents.js +97 -0
- package/dist/sync/types.d.ts +18 -0
- package/package.json +3 -2
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
6
|
+
import { ConvexError } from "convex/values";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
import { convexUrl, webApiUrl } from "../../cloud.js";
|
|
9
|
+
import { api } from "../../convex.js";
|
|
10
|
+
import { extractFrontmatterBlock, stripFrontmatterBlock, } from "../../schema/parser-core.js";
|
|
11
|
+
import { refusal } from "./client.js";
|
|
12
|
+
import { layOut, recordSavedIds, } from "./working-folder.js";
|
|
13
|
+
/**
|
|
14
|
+
* A solution's share command, from any repository (SOLSHARE; design in
|
|
15
|
+
* docs/working/solution-branches.md, "Handing the build to the people who
|
|
16
|
+
* do it"): `dotreq solution checkout --share <token> --outcome "<name>"`
|
|
17
|
+
* lays the solution's work out and records, under the solution's folder,
|
|
18
|
+
* which solution, outcome and round it was for; `dotreq solution push` sends the
|
|
19
|
+
* changed documents to the solution's branch; `dotreq solution done` closes
|
|
20
|
+
* the round it was checked out for. The token is the only credential, and
|
|
21
|
+
* the record stays out of git.
|
|
22
|
+
*/
|
|
23
|
+
const RECORD = ".checkout.json";
|
|
24
|
+
const SOLUTIONS = path.join(".requirements", "solutions");
|
|
25
|
+
/** What stays out of git in a solution's folder: the record, which holds the token, and the rendered mockups. */
|
|
26
|
+
const IGNORED = [RECORD, "mockups/"];
|
|
27
|
+
/**
|
|
28
|
+
* The verbs as the share command runs them, with no install: a developer
|
|
29
|
+
* who ran `npx` has no `dotreq` on their path.
|
|
30
|
+
*/
|
|
31
|
+
export const SHARE_CLI = "npx @popoverai/dotrequirements@latest solution";
|
|
32
|
+
/**
|
|
33
|
+
* Work from the repository's root, whichever folder the command was run
|
|
34
|
+
* in, so that a checkout from a subfolder and a push from the root see the
|
|
35
|
+
* same record. Outside a git repository, where it was run.
|
|
36
|
+
*/
|
|
37
|
+
function atRepositoryRoot() {
|
|
38
|
+
try {
|
|
39
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
+
}).trim();
|
|
43
|
+
if (root)
|
|
44
|
+
process.chdir(root);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Not a git repository: the folder it was run in is the repository.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Calls on the share command's token alone: no project is linked, and none is needed. */
|
|
51
|
+
function shareClient() {
|
|
52
|
+
const client = new ConvexHttpClient(convexUrl());
|
|
53
|
+
const inWords = async (call) => {
|
|
54
|
+
try {
|
|
55
|
+
return await call();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof ConvexError)
|
|
59
|
+
throw new Error(refusal(error.data));
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
query: (fn, args) => inWords(() => client.query(fn, args)),
|
|
65
|
+
mutation: (fn, args) => inWords(() => client.mutation(fn, args)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function hashOf(file) {
|
|
69
|
+
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
function recordPath(directory) {
|
|
72
|
+
return path.join(SOLUTIONS, directory, RECORD);
|
|
73
|
+
}
|
|
74
|
+
/** Every solution checked out into this repository, by its folder. */
|
|
75
|
+
export function checkedOut() {
|
|
76
|
+
if (!fs.existsSync(SOLUTIONS))
|
|
77
|
+
return [];
|
|
78
|
+
return fs
|
|
79
|
+
.readdirSync(SOLUTIONS)
|
|
80
|
+
.map((directory) => recordPath(directory))
|
|
81
|
+
.filter((file) => fs.existsSync(file))
|
|
82
|
+
.map((file) => JSON.parse(fs.readFileSync(file, "utf-8")));
|
|
83
|
+
}
|
|
84
|
+
/** The one solution a push or done is for, or why it can't tell (SOLSHARE-3.7, 4.6, 4.7). */
|
|
85
|
+
function oneCheckedOut(solution) {
|
|
86
|
+
const records = checkedOut();
|
|
87
|
+
if (records.length === 0) {
|
|
88
|
+
throw new Error("No solution is checked out here: run the share command you were given first.");
|
|
89
|
+
}
|
|
90
|
+
if (solution) {
|
|
91
|
+
const found = records.find((record) => record.solution.id === solution ||
|
|
92
|
+
record.solution.directory === solution);
|
|
93
|
+
if (!found) {
|
|
94
|
+
throw new Error(`"${solution}" isn't checked out here. Checked out: ${records.map((record) => record.solution.directory).join(", ")}.`);
|
|
95
|
+
}
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
if (records.length > 1) {
|
|
99
|
+
throw new Error(`More than one solution is checked out here; say which with --solution: ${records.map((record) => record.solution.directory).join(", ")}.`);
|
|
100
|
+
}
|
|
101
|
+
return records[0];
|
|
102
|
+
}
|
|
103
|
+
function writeRecord(record) {
|
|
104
|
+
const directory = path.join(SOLUTIONS, record.solution.directory);
|
|
105
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
106
|
+
fs.writeFileSync(recordPath(record.solution.directory), `${JSON.stringify(record, null, 2)}\n`);
|
|
107
|
+
// The record holds the token, and the mockups are renders: both stay out
|
|
108
|
+
// of the repository's history. Lines the developer added are kept.
|
|
109
|
+
const ignore = path.join(directory, ".gitignore");
|
|
110
|
+
const lines = fs.existsSync(ignore)
|
|
111
|
+
? fs.readFileSync(ignore, "utf-8").split("\n").filter(Boolean)
|
|
112
|
+
: [];
|
|
113
|
+
const missing = IGNORED.filter((line) => !lines.includes(line));
|
|
114
|
+
if (missing.length > 0) {
|
|
115
|
+
fs.writeFileSync(ignore, `${[...lines, ...missing].join("\n")}\n`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Markdown files under `.requirements/`, with the document id each one's
|
|
120
|
+
* frontmatter names. A file whose frontmatter can't be read is set aside,
|
|
121
|
+
* with a warning, rather than stopping the command.
|
|
122
|
+
*/
|
|
123
|
+
function requirementsFiles() {
|
|
124
|
+
const found = [];
|
|
125
|
+
const walk = (directory) => {
|
|
126
|
+
if (!fs.existsSync(directory))
|
|
127
|
+
return;
|
|
128
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
129
|
+
const full = path.join(directory, entry.name);
|
|
130
|
+
if (entry.isDirectory())
|
|
131
|
+
walk(full);
|
|
132
|
+
else if (entry.name.endsWith(".md")) {
|
|
133
|
+
let id;
|
|
134
|
+
try {
|
|
135
|
+
const yaml = extractFrontmatterBlock(fs.readFileSync(full, "utf-8"));
|
|
136
|
+
id = (yaml ? parse(yaml) : null)?.document?.id;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.warn(`Left ${full} alone: its frontmatter isn't YAML the command can read.`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
found.push({
|
|
143
|
+
file: full,
|
|
144
|
+
...(typeof id === "string" ? { documentId: id } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
walk(".requirements");
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
/** A file as a push submits it: its text without frontmatter, and what the frontmatter says of its document. */
|
|
153
|
+
function submitted(file) {
|
|
154
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
155
|
+
const yaml = extractFrontmatterBlock(content);
|
|
156
|
+
const document = (yaml ? parse(yaml) : null)?.document;
|
|
157
|
+
return {
|
|
158
|
+
path: file,
|
|
159
|
+
markdownContent: stripFrontmatterBlock(content),
|
|
160
|
+
...(typeof document?.id === "string" ? { documentId: document.id } : {}),
|
|
161
|
+
...(typeof document?.defaultPrefix === "string"
|
|
162
|
+
? { defaultPrefix: document.defaultPrefix }
|
|
163
|
+
: {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** The app the share command renders mockups through. */
|
|
167
|
+
function appUrl() {
|
|
168
|
+
return webApiUrl().replace(/\/+$/, "");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `dotreq solution checkout --share <token> --outcome "<name>"`
|
|
172
|
+
* (SOLSHARE-2): every document of the project into `.requirements/`, the
|
|
173
|
+
* solution's branch's versions where it has them, each at the file this
|
|
174
|
+
* repository already has for it or where the project's sync would put it;
|
|
175
|
+
* the solution's mockups as pages to open; the record push and done act
|
|
176
|
+
* on; and the outcome's prompt, printed for whoever or whatever reads the
|
|
177
|
+
* output.
|
|
178
|
+
*/
|
|
179
|
+
export async function shareCheckout(token,
|
|
180
|
+
/** The outcome handed over: one someone outside the product creates. */
|
|
181
|
+
outcome,
|
|
182
|
+
/** SOLSHARE-2.10: the branch's version replaces an edit it has a newer version than. */
|
|
183
|
+
branchWins = false) {
|
|
184
|
+
if (!outcome)
|
|
185
|
+
throw new Error('Say which outcome: --outcome "<name>", as the command you were given does.');
|
|
186
|
+
atRepositoryRoot();
|
|
187
|
+
const client = shareClient();
|
|
188
|
+
const got = await client.query(api.solutions.shares.forCheckout, {
|
|
189
|
+
token,
|
|
190
|
+
outcome,
|
|
191
|
+
});
|
|
192
|
+
// SOLSHARE-2.6, 2.10, 2.11: an edit that wasn't pushed is left as it is,
|
|
193
|
+
// unless the branch has saved a newer version of its document since the
|
|
194
|
+
// checkout: then the checkout stops, or with --branch-wins takes the
|
|
195
|
+
// branch's version.
|
|
196
|
+
const earlier = checkedOut().find((record) => record.solution.id === got.solution.id);
|
|
197
|
+
const unpushed = (earlier?.files ?? []).filter((file) => fs.existsSync(file.path) && hashOf(file.path) !== file.hash);
|
|
198
|
+
const current = new Map(got.files.map((file) => [file.documentId, file]));
|
|
199
|
+
const newer = (file) => {
|
|
200
|
+
const now = current.get(file.documentId);
|
|
201
|
+
return !!now?.onBranch && (!file.onBranch || now.version > file.version);
|
|
202
|
+
};
|
|
203
|
+
const conflicting = unpushed.filter(newer);
|
|
204
|
+
const kept = unpushed.filter((file) => !newer(file));
|
|
205
|
+
if (conflicting.length > 0 && !branchWins) {
|
|
206
|
+
throw new Error(`Nothing was written: the solution's branch has saved a newer version of ${conflicting.map((file) => file.path).join(", ")} since you checked ${conflicting.length === 1 ? "it" : "them"} out, and your changes to ${conflicting.length === 1 ? "it" : "them"} weren't pushed. Push them first with \`${SHARE_CLI} push --repo-wins\` to keep yours, or run this again with --branch-wins to take the branch's.`);
|
|
207
|
+
}
|
|
208
|
+
const keptIds = new Set(kept.map((file) => file.documentId));
|
|
209
|
+
// Each document goes where this repository already keeps it, when it does.
|
|
210
|
+
const here = new Map(requirementsFiles()
|
|
211
|
+
.filter((found) => found.documentId)
|
|
212
|
+
.map((found) => [found.documentId, found.file]));
|
|
213
|
+
const files = got.files.map((file) => ({
|
|
214
|
+
...file,
|
|
215
|
+
path: here.get(file.documentId) ?? file.path,
|
|
216
|
+
}));
|
|
217
|
+
layOut(files.filter((file) => !keptIds.has(file.documentId)));
|
|
218
|
+
const mockupDirectory = path.join(SOLUTIONS, got.solution.directory, "mockups");
|
|
219
|
+
const rendered = [];
|
|
220
|
+
const failed = [];
|
|
221
|
+
for (const mockupId of got.mockups) {
|
|
222
|
+
const response = await fetch(`${appUrl()}/api/solutions/share/mockup`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ token, mockupId }),
|
|
226
|
+
}).catch(() => null);
|
|
227
|
+
if (!response?.ok) {
|
|
228
|
+
failed.push(mockupId);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
fs.mkdirSync(mockupDirectory, { recursive: true });
|
|
232
|
+
fs.writeFileSync(path.join(mockupDirectory, `${mockupId}.html`), await response.text());
|
|
233
|
+
rendered.push(mockupId);
|
|
234
|
+
}
|
|
235
|
+
writeRecord({
|
|
236
|
+
token,
|
|
237
|
+
outcome: got.outcome,
|
|
238
|
+
round: got.round,
|
|
239
|
+
solution: got.solution,
|
|
240
|
+
// An edit left as it is keeps the version it was made from, so a push
|
|
241
|
+
// still sends it.
|
|
242
|
+
files: files.map((file) => kept.find((left) => left.documentId === file.documentId) ?? {
|
|
243
|
+
documentId: file.documentId,
|
|
244
|
+
path: file.path,
|
|
245
|
+
version: file.version,
|
|
246
|
+
onBranch: file.onBranch,
|
|
247
|
+
hash: hashOf(file.path),
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
console.log(`Checked out ${got.solution.title}: ${files.length} ${files.length === 1 ? "document" : "documents"} under .requirements/${rendered.length ? `, and ${rendered.length} ${rendered.length === 1 ? "mockup" : "mockups"} under ${mockupDirectory}/` : ""}.`);
|
|
251
|
+
if (kept.length > 0) {
|
|
252
|
+
console.log(`Left your changes to ${kept.map((file) => file.path).join(", ")} as they are: the branch hasn't changed ${kept.length === 1 ? "that document" : "those documents"} since.`);
|
|
253
|
+
}
|
|
254
|
+
if (conflicting.length > 0) {
|
|
255
|
+
console.log(`Replaced your changes to ${conflicting.map((file) => file.path).join(", ")} with the branch's newer ${conflicting.length === 1 ? "version" : "versions"}.`);
|
|
256
|
+
}
|
|
257
|
+
if (failed.length > 0) {
|
|
258
|
+
console.log(`Couldn't render ${failed.length === 1 ? "mockup" : "mockups"} ${failed.join(", ")}: run the share command again to try again.`);
|
|
259
|
+
}
|
|
260
|
+
if (got.prompt) {
|
|
261
|
+
console.log(`\n${got.prompt}`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.log(`\n"${got.outcome}" isn't waiting on anyone right now, so there's nothing to hand back yet.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `dotreq solution push`, in a repository a solution was checked out into
|
|
269
|
+
* (SOLSHARE-3): each document changed since the checkout, and each one
|
|
270
|
+
* added under `.requirements/`, to the solution's branch. False when no
|
|
271
|
+
* solution is checked out here, for the runner's push to take over.
|
|
272
|
+
*/
|
|
273
|
+
export async function sharePush(solution,
|
|
274
|
+
/** SOLSHARE-3.8: the repository's version wins over a newer one on the branch. */
|
|
275
|
+
repoWins = false) {
|
|
276
|
+
atRepositoryRoot();
|
|
277
|
+
if (checkedOut().length === 0)
|
|
278
|
+
return false;
|
|
279
|
+
const record = oneCheckedOut(solution);
|
|
280
|
+
const changed = record.files
|
|
281
|
+
.filter((file) => fs.existsSync(file.path) && hashOf(file.path) !== file.hash)
|
|
282
|
+
.map((file) => file.path);
|
|
283
|
+
// A new document is a requirements file, or any Markdown in a solution's
|
|
284
|
+
// folder, that names no document yet; other notes under .requirements/
|
|
285
|
+
// stay where they are.
|
|
286
|
+
const added = requirementsFiles()
|
|
287
|
+
.filter((found) => !found.documentId &&
|
|
288
|
+
(found.file.endsWith(".requirements.md") ||
|
|
289
|
+
found.file.startsWith(`${SOLUTIONS}${path.sep}`)))
|
|
290
|
+
.map((found) => found.file);
|
|
291
|
+
const files = [...changed, ...added].map(submitted);
|
|
292
|
+
if (files.length === 0) {
|
|
293
|
+
console.log("Nothing to push: no document changed since the checkout.");
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
const saved = await shareClient().mutation(api.solutions.shares.push, {
|
|
297
|
+
token: record.token,
|
|
298
|
+
files,
|
|
299
|
+
base: record.files.map(({ documentId, version, onBranch }) => ({
|
|
300
|
+
documentId,
|
|
301
|
+
version,
|
|
302
|
+
onBranch,
|
|
303
|
+
})),
|
|
304
|
+
...(repoWins ? { repoWins: true } : {}),
|
|
305
|
+
});
|
|
306
|
+
recordSavedIds(files, saved);
|
|
307
|
+
const byId = new Map(record.files.map((file) => [file.documentId, file]));
|
|
308
|
+
for (const entry of saved) {
|
|
309
|
+
byId.set(entry.documentId, {
|
|
310
|
+
documentId: entry.documentId,
|
|
311
|
+
path: entry.path,
|
|
312
|
+
version: entry.version,
|
|
313
|
+
onBranch: true,
|
|
314
|
+
hash: hashOf(entry.path),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
writeRecord({ ...record, files: [...byId.values()] });
|
|
318
|
+
for (const entry of saved) {
|
|
319
|
+
console.log(`Saved ${entry.path} on the solution's branch as version ${entry.version}.${entry.replaced ? ` It replaced version ${entry.replaced.version}, saved ${new Date(entry.replaced.savedAt).toLocaleString()}, which the branch keeps.` : ""}`);
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* `dotreq solution done [<where it is>...] [--comment "..."]` (SOLSHARE-4):
|
|
325
|
+
* close the round of the outcome the repository was checked out for, with
|
|
326
|
+
* nothing to paste.
|
|
327
|
+
*/
|
|
328
|
+
export async function doneCommand(made, options) {
|
|
329
|
+
atRepositoryRoot();
|
|
330
|
+
const record = oneCheckedOut(options.solution);
|
|
331
|
+
if (record.round === null) {
|
|
332
|
+
throw new Error(`"${record.outcome}" wasn't waiting on anyone when it was checked out: run the share command again once it is.`);
|
|
333
|
+
}
|
|
334
|
+
const said = await shareClient().mutation(api.solutions.shares.done, {
|
|
335
|
+
token: record.token,
|
|
336
|
+
outcome: record.outcome,
|
|
337
|
+
round: record.round,
|
|
338
|
+
...(made.length ? { made } : {}),
|
|
339
|
+
...(options.comment?.trim() ? { comment: options.comment.trim() } : {}),
|
|
340
|
+
});
|
|
341
|
+
// STEP-REVIEW-18.7.0: nothing was recorded, and the message says why.
|
|
342
|
+
if (said.recorded === false)
|
|
343
|
+
throw new Error(said.message);
|
|
344
|
+
console.log(said.message);
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=share.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type WhereOptions } from "./run-file.js";
|
|
2
|
+
/**
|
|
3
|
+
* `dotreq solution stall "<what has to change>"`: the agent cannot go on
|
|
4
|
+
* with the outcome it is at, or the one `--outcome` names, and no decision
|
|
5
|
+
* would fix that. The reason is what has to change before it can be
|
|
6
|
+
* started again (VERB-3.2).
|
|
7
|
+
*/
|
|
8
|
+
export declare function stallCommand(reason: string, options: WhereOptions & {
|
|
9
|
+
/** False from the runner, which pushed already and says why it couldn't in the reason. */
|
|
10
|
+
push?: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
/** A stall's reason, and that the work could not be pushed, and why (SOLBRANCH-3.4, 4.1). */
|
|
13
|
+
export declare function withUnpushed(reason: string, why: string): string;
|
|
14
|
+
//# sourceMappingURL=stall.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { api } from "../../convex.js";
|
|
2
|
+
import { solutionClient } from "./client.js";
|
|
3
|
+
import { pushWork } from "./pushing.js";
|
|
4
|
+
import { onOutcome, requireWhere } from "./run-file.js";
|
|
5
|
+
/**
|
|
6
|
+
* `dotreq solution stall "<what has to change>"`: the agent cannot go on
|
|
7
|
+
* with the outcome it is at, or the one `--outcome` names, and no decision
|
|
8
|
+
* would fix that. The reason is what has to change before it can be
|
|
9
|
+
* started again (VERB-3.2).
|
|
10
|
+
*/
|
|
11
|
+
export async function stallCommand(reason, options) {
|
|
12
|
+
if (!reason.trim()) {
|
|
13
|
+
throw new Error("Say what has to change before your work can be started again.");
|
|
14
|
+
}
|
|
15
|
+
const where = requireWhere(options);
|
|
16
|
+
const { solutionId, agent } = where;
|
|
17
|
+
const client = solutionClient();
|
|
18
|
+
// SOLBRANCH-3.4: a stall is recorded whether or not the work can be
|
|
19
|
+
// pushed; when it can't, the reason Quinn reads says so, and why.
|
|
20
|
+
let said = reason.trim();
|
|
21
|
+
if (where.branch && options.push !== false) {
|
|
22
|
+
const pushed = await pushWork(client, {
|
|
23
|
+
solutionId,
|
|
24
|
+
branch: where.branch,
|
|
25
|
+
});
|
|
26
|
+
if (!pushed.ok)
|
|
27
|
+
said = withUnpushed(said, pushed.why);
|
|
28
|
+
}
|
|
29
|
+
await client.mutation(api.solutions.mutations.stall, {
|
|
30
|
+
solutionId,
|
|
31
|
+
agent,
|
|
32
|
+
...(where.outcome ? { outcome: where.outcome } : {}),
|
|
33
|
+
reason: said,
|
|
34
|
+
});
|
|
35
|
+
console.log(`Stalled${onOutcome(where.outcome)}. It can be started again once that is fixed.`);
|
|
36
|
+
}
|
|
37
|
+
/** A stall's reason, and that the work could not be pushed, and why (SOLBRANCH-3.4, 4.1). */
|
|
38
|
+
export function withUnpushed(reason, why) {
|
|
39
|
+
return `${reason.replace(/\s+$/, "")}${/[.!?]$/.test(reason.trim()) ? "" : "."} The work could not be pushed: ${why.replace(/[.\s]+$/, "")}.`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=stall.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotreq solution start <solution-id> <agent>`: begin an agent's work by
|
|
3
|
+
* hand. A stalled agent is started again, and it takes the part of an
|
|
4
|
+
* outcome's work it has due: the one `--outcome` names, or its first in the
|
|
5
|
+
* plan's order.
|
|
6
|
+
*/
|
|
7
|
+
export declare function startCommand(solutionId: string, agent: string, options: {
|
|
8
|
+
outcome?: string;
|
|
9
|
+
step?: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=start.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { api } from "../../convex.js";
|
|
2
|
+
import { solutionClient } from "./client.js";
|
|
3
|
+
import { onOutcome } from "./run-file.js";
|
|
4
|
+
/**
|
|
5
|
+
* `dotreq solution start <solution-id> <agent>`: begin an agent's work by
|
|
6
|
+
* hand. A stalled agent is started again, and it takes the part of an
|
|
7
|
+
* outcome's work it has due: the one `--outcome` names, or its first in the
|
|
8
|
+
* plan's order.
|
|
9
|
+
*/
|
|
10
|
+
export async function startCommand(solutionId, agent, options) {
|
|
11
|
+
const outcome = (options.outcome ?? options.step)?.trim() || undefined;
|
|
12
|
+
const client = solutionClient();
|
|
13
|
+
await client.mutation(api.solutions.mutations.start, {
|
|
14
|
+
solutionId,
|
|
15
|
+
agent,
|
|
16
|
+
...(outcome ? { outcome } : {}),
|
|
17
|
+
});
|
|
18
|
+
console.log(`Started ${agent}${onOutcome(outcome)}.`);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Panel, type PanelOutcome } from "./brief.js";
|
|
2
|
+
import { solutionClient } from "./client.js";
|
|
3
|
+
import { type WhereOptions } from "./run-file.js";
|
|
4
|
+
/** What an agent is at: an outcome, and whether it drafts or reviews it there. */
|
|
5
|
+
export type AgentAt = {
|
|
6
|
+
outcome: PanelOutcome;
|
|
7
|
+
part: "draft" | "review";
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The outcome an agent is at, read off the plan panel: the one named, when
|
|
11
|
+
* `--outcome` names it; otherwise the one it is drafting, else one it
|
|
12
|
+
* reviews that stands reviewing, else one it stalled or asked on, else one
|
|
13
|
+
* ready for it to draft. Undefined when it is at none, as after it recorded
|
|
14
|
+
* its work; refused when it is at more than one and nothing says which.
|
|
15
|
+
*/
|
|
16
|
+
export declare function agentAt(panel: Pick<Panel, "phases">, agent: string, named?: string): AgentAt | undefined;
|
|
17
|
+
/** The plan panel, or a refusal when the solution isn't there. */
|
|
18
|
+
export declare function panelOf(client: ReturnType<typeof solutionClient>, solutionId: string): Promise<Panel>;
|
|
19
|
+
/**
|
|
20
|
+
* The outcome a verb is about: the one named, else the one the agent is at
|
|
21
|
+
* on the panel. Refused, saying how to name it, when it is at none.
|
|
22
|
+
*/
|
|
23
|
+
export declare function outcomeFor(client: ReturnType<typeof solutionClient>, where: {
|
|
24
|
+
solutionId: string;
|
|
25
|
+
agent: string;
|
|
26
|
+
outcome?: string;
|
|
27
|
+
}, what: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* `dotreq solution status`: where the outcome the agent is at stands, as
|
|
30
|
+
* recorded — its one word, with the reason when stalled. With `--json`,
|
|
31
|
+
* also which outcome that is, whether the agent drafts or reviews it there,
|
|
32
|
+
* and whether it still owes its part: what the Stop hook and the runner's
|
|
33
|
+
* read-back judge an agent's session by (STEP-REVIEW-9). An agent at no
|
|
34
|
+
* outcome, as after it recorded its work, owes nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function statusCommand(options: WhereOptions & {
|
|
37
|
+
json?: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { api } from "../../convex.js";
|
|
2
|
+
import { outcomesOf } from "./brief.js";
|
|
3
|
+
import { solutionClient } from "./client.js";
|
|
4
|
+
import { requireWhere } from "./run-file.js";
|
|
5
|
+
const drafts = (outcome, agent) => outcome.drafter === agent;
|
|
6
|
+
const reviews = (outcome, agent) => (outcome.reviewers ?? []).includes(agent);
|
|
7
|
+
/**
|
|
8
|
+
* The outcome an agent is at, read off the plan panel: the one named, when
|
|
9
|
+
* `--outcome` names it; otherwise the one it is drafting, else one it
|
|
10
|
+
* reviews that stands reviewing, else one it stalled or asked on, else one
|
|
11
|
+
* ready for it to draft. Undefined when it is at none, as after it recorded
|
|
12
|
+
* its work; refused when it is at more than one and nothing says which.
|
|
13
|
+
*/
|
|
14
|
+
export function agentAt(panel, agent, named) {
|
|
15
|
+
const outcomes = outcomesOf(panel);
|
|
16
|
+
if (named) {
|
|
17
|
+
const outcome = outcomes.find((each) => each.name === named);
|
|
18
|
+
if (!outcome) {
|
|
19
|
+
throw new Error(`The plan has no outcome called "${named}". Its outcomes: ${outcomes.map((each) => each.name).join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
const drafting = drafts(outcome, agent);
|
|
22
|
+
const reviewing = reviews(outcome, agent);
|
|
23
|
+
if (!drafting && !reviewing) {
|
|
24
|
+
throw new Error(`${agent} neither drafts nor reviews "${named}".`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
outcome,
|
|
28
|
+
part: reviewing && (!drafting || outcome.status === "reviewing")
|
|
29
|
+
? "review"
|
|
30
|
+
: "draft",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const tiers = [
|
|
34
|
+
(outcome) => drafts(outcome, agent) && outcome.status === "drafting"
|
|
35
|
+
? { outcome, part: "draft" }
|
|
36
|
+
: undefined,
|
|
37
|
+
(outcome) => reviews(outcome, agent) && outcome.status === "reviewing"
|
|
38
|
+
? { outcome, part: "review" }
|
|
39
|
+
: undefined,
|
|
40
|
+
(outcome) => outcome.status === "stalled" || outcome.status === "awaiting decision"
|
|
41
|
+
? drafts(outcome, agent)
|
|
42
|
+
? { outcome, part: "draft" }
|
|
43
|
+
: reviews(outcome, agent)
|
|
44
|
+
? { outcome, part: "review" }
|
|
45
|
+
: undefined
|
|
46
|
+
: undefined,
|
|
47
|
+
(outcome) => drafts(outcome, agent) && outcome.status === "ready"
|
|
48
|
+
? { outcome, part: "draft" }
|
|
49
|
+
: undefined,
|
|
50
|
+
];
|
|
51
|
+
for (const tier of tiers) {
|
|
52
|
+
const found = outcomes.flatMap((outcome) => {
|
|
53
|
+
const at = tier(outcome);
|
|
54
|
+
return at ? [at] : [];
|
|
55
|
+
});
|
|
56
|
+
if (found.length === 1)
|
|
57
|
+
return found[0];
|
|
58
|
+
if (found.length > 1) {
|
|
59
|
+
throw new Error(`${agent} is at more than one outcome: ${found.map((at) => `"${at.outcome.name}"`).join(", ")}. Name one with --outcome "<name>".`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The plan panel, or a refusal when the solution isn't there. */
|
|
65
|
+
export async function panelOf(client, solutionId) {
|
|
66
|
+
const panel = await client.query(api.solutions.queries.getPlan, {
|
|
67
|
+
solutionId,
|
|
68
|
+
});
|
|
69
|
+
if (!panel)
|
|
70
|
+
throw new Error("Solution not found.");
|
|
71
|
+
return panel;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The outcome a verb is about: the one named, else the one the agent is at
|
|
75
|
+
* on the panel. Refused, saying how to name it, when it is at none.
|
|
76
|
+
*/
|
|
77
|
+
export async function outcomeFor(client, where, what) {
|
|
78
|
+
if (where.outcome)
|
|
79
|
+
return where.outcome;
|
|
80
|
+
const at = agentAt(await panelOf(client, where.solutionId), where.agent);
|
|
81
|
+
if (!at) {
|
|
82
|
+
throw new Error(`${where.agent} is at no outcome now, so nothing says which ${what}. Name it with --outcome "<name>".`);
|
|
83
|
+
}
|
|
84
|
+
return at.outcome.name;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a reviewer still owes its result on an outcome standing
|
|
88
|
+
* reviewing: an outcome with several reviewers stands reviewing until the
|
|
89
|
+
* last gives its result, so one that gave its own since the latest
|
|
90
|
+
* submission owes nothing more.
|
|
91
|
+
*/
|
|
92
|
+
async function reviewerOwes(client, solutionId, outcome, agent) {
|
|
93
|
+
const thread = (await client.query(api.solutions.queries.getThread, {
|
|
94
|
+
solutionId,
|
|
95
|
+
outcome,
|
|
96
|
+
})) ?? [];
|
|
97
|
+
let since = 0;
|
|
98
|
+
thread.forEach((entry, at) => {
|
|
99
|
+
if (entry.kind === "submission")
|
|
100
|
+
since = at + 1;
|
|
101
|
+
});
|
|
102
|
+
return !thread
|
|
103
|
+
.slice(since)
|
|
104
|
+
.some((entry) => entry.agent === agent &&
|
|
105
|
+
(entry.kind === "review" || entry.kind === "escalation"));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `dotreq solution status`: where the outcome the agent is at stands, as
|
|
109
|
+
* recorded — its one word, with the reason when stalled. With `--json`,
|
|
110
|
+
* also which outcome that is, whether the agent drafts or reviews it there,
|
|
111
|
+
* and whether it still owes its part: what the Stop hook and the runner's
|
|
112
|
+
* read-back judge an agent's session by (STEP-REVIEW-9). An agent at no
|
|
113
|
+
* outcome, as after it recorded its work, owes nothing.
|
|
114
|
+
*/
|
|
115
|
+
export async function statusCommand(options) {
|
|
116
|
+
const where = requireWhere(options);
|
|
117
|
+
const client = solutionClient();
|
|
118
|
+
const panel = await panelOf(client, where.solutionId);
|
|
119
|
+
const at = agentAt(panel, where.agent, where.outcome);
|
|
120
|
+
if (!at) {
|
|
121
|
+
if (options.json) {
|
|
122
|
+
process.stdout.write(`${JSON.stringify({ agent: where.agent, outcome: null, part: null, status: null, reason: null, reviews: false, owes: false })}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
process.stdout.write(`${where.agent} is at no outcome now.\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { outcome, part } = at;
|
|
129
|
+
// CHECKPOINT-6.3.2.3: a drafter whose work was set aside owes nothing
|
|
130
|
+
// more there; the new draft is another session's.
|
|
131
|
+
const setAside = part === "draft" && outcome.setAside === true;
|
|
132
|
+
const owes = part === "draft"
|
|
133
|
+
? !setAside &&
|
|
134
|
+
(outcome.status === "drafting" || outcome.status === "ready")
|
|
135
|
+
: outcome.status === "reviewing" &&
|
|
136
|
+
(await reviewerOwes(client, where.solutionId, outcome.name, where.agent));
|
|
137
|
+
if (options.json) {
|
|
138
|
+
process.stdout.write(`${JSON.stringify({ agent: where.agent, outcome: outcome.name, part, status: outcome.status, reason: outcome.reason ?? null, reviews: part === "review", ...(setAside ? { setAside } : {}), owes })}\n`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
process.stdout.write(outcome.reason
|
|
142
|
+
? `${outcome.name}: ${outcome.status} — ${outcome.reason}\n`
|
|
143
|
+
: `${outcome.name}: ${outcome.status}\n`);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executive summary a verb carries (STEP-REVIEW-20): for the owner,
|
|
3
|
+
* what the work is and what most needs their eye, where the comment is for
|
|
4
|
+
* the thread. `--executive-summary` is its name; `--summary` is short for
|
|
5
|
+
* it, for a person typing it by hand (VERB-1.6, 2.5).
|
|
6
|
+
*/
|
|
7
|
+
export type SummaryOptions = {
|
|
8
|
+
executiveSummary?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
};
|
|
11
|
+
/** The summary given, by either name; undefined when none, or only blanks. */
|
|
12
|
+
export declare function summaryGiven(options: SummaryOptions): string | undefined;
|
|
13
|
+
//# sourceMappingURL=summary.d.ts.map
|