dw-mc 0.3.0 → 0.4.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/NOTICE.md +1 -1
- package/README.md +27 -30
- package/dist/bin.js +398 -578
- package/dist/bin.js.map +1 -1
- package/package.json +1 -2
package/dist/bin.js
CHANGED
|
@@ -114,24 +114,18 @@ const encodeYaml = (value) => {
|
|
|
114
114
|
//#endregion
|
|
115
115
|
//#region src/adapters/config.ts
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
* review prompt on one of the two agent CLIs.
|
|
117
|
+
* How much a review run spends, in the words the slash command takes.
|
|
119
118
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
119
|
+
* The set is Claude Code's and not this tool's, so it is wider than the three
|
|
120
|
+
* words a review used to be held to: a run that would be worth `max` is one I
|
|
121
|
+
* should be able to ask for without spelling the whole command out.
|
|
123
122
|
*/
|
|
124
|
-
const runners = [
|
|
125
|
-
"builtin",
|
|
126
|
-
"prompt",
|
|
127
|
-
"codex"
|
|
128
|
-
];
|
|
129
|
-
const Runner = Schema.Literals(runners);
|
|
130
|
-
/** How much a built-in review run spends. */
|
|
131
123
|
const Effort = Schema.Literals([
|
|
132
124
|
"low",
|
|
133
125
|
"medium",
|
|
134
|
-
"high"
|
|
126
|
+
"high",
|
|
127
|
+
"xhigh",
|
|
128
|
+
"max"
|
|
135
129
|
]);
|
|
136
130
|
/** How much a finding weighs. */
|
|
137
131
|
const Severity = Schema.Literals([
|
|
@@ -139,10 +133,6 @@ const Severity = Schema.Literals([
|
|
|
139
133
|
"warning",
|
|
140
134
|
"info"
|
|
141
135
|
]);
|
|
142
|
-
const PathInstruction = Schema.Struct({
|
|
143
|
-
path: Schema.String,
|
|
144
|
-
instructions: Schema.String
|
|
145
|
-
});
|
|
146
136
|
/**
|
|
147
137
|
* What one section of the file may say. Every key is optional: what the file
|
|
148
138
|
* leaves out is inherited rather than reset, so `defaults` and a repository's
|
|
@@ -151,12 +141,11 @@ const PathInstruction = Schema.Struct({
|
|
|
151
141
|
const SettingsPatch = Schema.Struct({
|
|
152
142
|
base: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
153
143
|
review: Schema.optionalKey(Schema.Struct({
|
|
154
|
-
|
|
155
|
-
effort: Schema.optionalKey(Effort),
|
|
144
|
+
command: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
145
|
+
effort: Schema.optionalKey(Schema.NullOr(Effort)),
|
|
146
|
+
prompt: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
156
147
|
model: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
157
|
-
|
|
158
|
-
docs_only: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
159
|
-
path_instructions: Schema.optionalKey(Schema.Array(PathInstruction))
|
|
148
|
+
docs_only: Schema.optionalKey(Schema.Array(Schema.String))
|
|
160
149
|
})),
|
|
161
150
|
ci: Schema.optionalKey(Schema.Struct({
|
|
162
151
|
ignore: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
@@ -164,16 +153,12 @@ const SettingsPatch = Schema.Struct({
|
|
|
164
153
|
})),
|
|
165
154
|
fix: Schema.optionalKey(Schema.Struct({ commits: Schema.optionalKey(Schema.Boolean) })),
|
|
166
155
|
rebase: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),
|
|
167
|
-
stamp: Schema.optionalKey(Schema.Struct({
|
|
168
|
-
blocks_on: Schema.optionalKey(Severity),
|
|
169
|
-
supporting_blocks: Schema.optionalKey(Schema.Boolean)
|
|
170
|
-
}))
|
|
156
|
+
stamp: Schema.optionalKey(Schema.Struct({ blocks_on: Schema.optionalKey(Severity) }))
|
|
171
157
|
});
|
|
172
|
-
/** How this machine starts
|
|
158
|
+
/** How this machine starts Claude Code, where it does not start `claude` itself. */
|
|
173
159
|
const LauncherPatch = Schema.Struct({
|
|
174
160
|
command: Schema.optionalKey(Schema.Array(Schema.String).pipe(Schema.check(Schema.isMinLength(1, { message: "Expected the launcher command to name a program" })))),
|
|
175
|
-
fix_args: Schema.optionalKey(Schema.Array(Schema.String))
|
|
176
|
-
codex: Schema.optionalKey(Schema.Array(Schema.String).pipe(Schema.check(Schema.isMinLength(1, { message: "Expected the codex command to name a program" }))))
|
|
161
|
+
fix_args: Schema.optionalKey(Schema.Array(Schema.String))
|
|
177
162
|
});
|
|
178
163
|
/** A repository, as `gh` spells it: `owner/name`. */
|
|
179
164
|
const Repo = Schema.String.pipe(Schema.check(Schema.isPattern(/^[^\s/]+\/[^\s/]+$/, { message: "Expected a repository as owner/name" })));
|
|
@@ -187,12 +172,11 @@ const ConfigFile = Schema.Struct({
|
|
|
187
172
|
const builtIn = {
|
|
188
173
|
base: null,
|
|
189
174
|
review: {
|
|
190
|
-
|
|
175
|
+
command: "/code-review",
|
|
191
176
|
effort: "low",
|
|
177
|
+
prompt: null,
|
|
192
178
|
model: null,
|
|
193
|
-
|
|
194
|
-
docs_only: ["**/*.md", "docs/**"],
|
|
195
|
-
path_instructions: []
|
|
179
|
+
docs_only: ["**/*.md", "docs/**"]
|
|
196
180
|
},
|
|
197
181
|
ci: {
|
|
198
182
|
ignore: [],
|
|
@@ -200,16 +184,12 @@ const builtIn = {
|
|
|
200
184
|
},
|
|
201
185
|
fix: { commits: false },
|
|
202
186
|
rebase: { enabled: false },
|
|
203
|
-
stamp: {
|
|
204
|
-
blocks_on: "error",
|
|
205
|
-
supporting_blocks: false
|
|
206
|
-
}
|
|
187
|
+
stamp: { blocks_on: "error" }
|
|
207
188
|
};
|
|
208
|
-
/** `claude`
|
|
189
|
+
/** `claude` itself, which is what a machine that spawns it directly needs. */
|
|
209
190
|
const builtInLauncher = {
|
|
210
191
|
command: ["claude"],
|
|
211
|
-
fix_args: []
|
|
212
|
-
codex: ["codex"]
|
|
192
|
+
fix_args: []
|
|
213
193
|
};
|
|
214
194
|
/**
|
|
215
195
|
* The patch's value where it has one, the inherited value otherwise. A key the
|
|
@@ -220,12 +200,11 @@ const over = (patch, inherited) => patch === void 0 ? inherited : patch;
|
|
|
220
200
|
const apply = (settings, patch) => patch === void 0 ? settings : {
|
|
221
201
|
base: over(patch.base, settings.base),
|
|
222
202
|
review: {
|
|
223
|
-
|
|
203
|
+
command: over(patch.review?.command, settings.review.command),
|
|
224
204
|
effort: over(patch.review?.effort, settings.review.effort),
|
|
205
|
+
prompt: over(patch.review?.prompt, settings.review.prompt),
|
|
225
206
|
model: over(patch.review?.model, settings.review.model),
|
|
226
|
-
|
|
227
|
-
docs_only: over(patch.review?.docs_only, settings.review.docs_only),
|
|
228
|
-
path_instructions: over(patch.review?.path_instructions, settings.review.path_instructions)
|
|
207
|
+
docs_only: over(patch.review?.docs_only, settings.review.docs_only)
|
|
229
208
|
},
|
|
230
209
|
ci: {
|
|
231
210
|
ignore: over(patch.ci?.ignore, settings.ci.ignore),
|
|
@@ -233,10 +212,7 @@ const apply = (settings, patch) => patch === void 0 ? settings : {
|
|
|
233
212
|
},
|
|
234
213
|
fix: { commits: over(patch.fix?.commits, settings.fix.commits) },
|
|
235
214
|
rebase: { enabled: over(patch.rebase?.enabled, settings.rebase.enabled) },
|
|
236
|
-
stamp: {
|
|
237
|
-
blocks_on: over(patch.stamp?.blocks_on, settings.stamp.blocks_on),
|
|
238
|
-
supporting_blocks: over(patch.stamp?.supporting_blocks, settings.stamp.supporting_blocks)
|
|
239
|
-
}
|
|
215
|
+
stamp: { blocks_on: over(patch.stamp?.blocks_on, settings.stamp.blocks_on) }
|
|
240
216
|
};
|
|
241
217
|
/**
|
|
242
218
|
* `delta` over `patch`, keeping every key `delta` does not mention.
|
|
@@ -290,18 +266,16 @@ const withRepo = (file, repo, patch) => ({
|
|
|
290
266
|
}
|
|
291
267
|
});
|
|
292
268
|
/**
|
|
293
|
-
* What this machine starts
|
|
269
|
+
* What this machine starts Claude Code with: the file's launcher over `claude`.
|
|
294
270
|
*
|
|
295
271
|
* It is no repository's business. What spawns the agent CLI is a fact of the
|
|
296
272
|
* machine, which is why it sits beside `defaults` rather than inside it.
|
|
297
273
|
*/
|
|
298
274
|
const launcherOf = (file) => {
|
|
299
275
|
const [program = builtInLauncher.command[0], ...prefix] = file.launcher?.command ?? [];
|
|
300
|
-
const [codex = builtInLauncher.codex[0], ...codexPrefix] = file.launcher?.codex ?? [];
|
|
301
276
|
return {
|
|
302
277
|
command: [program, ...prefix],
|
|
303
|
-
fix_args: file.launcher?.fix_args ?? builtInLauncher.fix_args
|
|
304
|
-
codex: [codex, ...codexPrefix]
|
|
278
|
+
fix_args: file.launcher?.fix_args ?? builtInLauncher.fix_args
|
|
305
279
|
};
|
|
306
280
|
};
|
|
307
281
|
/** What `repo` is worth: its own overrides over the global defaults. */
|
|
@@ -349,6 +323,43 @@ var ConfigMalformed = class extends Schema.TaggedError()("ConfigMalformed", {
|
|
|
349
323
|
}
|
|
350
324
|
};
|
|
351
325
|
const reasonOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
326
|
+
/** The keys an earlier version had, read off a file loosely enough to find them. */
|
|
327
|
+
const LegacySection = Schema.Struct({
|
|
328
|
+
review: Schema.optionalKey(Schema.Struct({
|
|
329
|
+
runners: Schema.optionalKey(Schema.Unknown),
|
|
330
|
+
skill: Schema.optionalKey(Schema.Unknown),
|
|
331
|
+
path_instructions: Schema.optionalKey(Schema.Unknown)
|
|
332
|
+
})),
|
|
333
|
+
stamp: Schema.optionalKey(Schema.Struct({ supporting_blocks: Schema.optionalKey(Schema.Unknown) }))
|
|
334
|
+
});
|
|
335
|
+
const Legacy = Schema.Struct({
|
|
336
|
+
launcher: Schema.optionalKey(Schema.Struct({ codex: Schema.optionalKey(Schema.Unknown) })),
|
|
337
|
+
defaults: Schema.optionalKey(LegacySection),
|
|
338
|
+
repos: Schema.optionalKey(Schema.Record(Schema.String, LegacySection))
|
|
339
|
+
});
|
|
340
|
+
const asLegacy = Schema.decodeUnknownOption(Legacy);
|
|
341
|
+
/**
|
|
342
|
+
* What a file from an earlier version says, and what to do about each of it.
|
|
343
|
+
*
|
|
344
|
+
* The excess-property error names a key and stops there, which is enough for a
|
|
345
|
+
* key that is simply gone and not enough for one that moved: `review.skill` is
|
|
346
|
+
* `review.prompt` now, and a file quietly stripped of it is a review brief lost.
|
|
347
|
+
* This is here to be deleted once no file has those keys left.
|
|
348
|
+
*/
|
|
349
|
+
const legacyIn = (decided) => {
|
|
350
|
+
const legacy = asLegacy(decided);
|
|
351
|
+
if (Option.isNone(legacy)) return null;
|
|
352
|
+
const sections = [legacy.value.defaults, ...Object.values(legacy.value.repos ?? {})];
|
|
353
|
+
const spelled = (says) => sections.some((section) => section !== void 0 && says(section) !== void 0);
|
|
354
|
+
const said = [
|
|
355
|
+
legacy.value.launcher?.codex === void 0 ? null : "launcher.codex is gone: reviews run on Claude Code alone.",
|
|
356
|
+
spelled((section) => section.review?.runners) ? "review.runners is gone: a head carries one review run, which review.command configures." : null,
|
|
357
|
+
spelled((section) => section.review?.skill) ? "review.skill is review.prompt now, unchanged in what it does - move the text across rather than losing it." : null,
|
|
358
|
+
spelled((section) => section.review?.path_instructions) ? "review.path_instructions is gone: nothing ever read it." : null,
|
|
359
|
+
spelled((section) => section.stamp?.supporting_blocks) ? "stamp.supporting_blocks is gone: there is no second opinion to let through." : null
|
|
360
|
+
].filter((sentence) => sentence !== null);
|
|
361
|
+
return said.length === 0 ? null : `it names keys this version does not have.\n${said.join("\n")}`;
|
|
362
|
+
};
|
|
352
363
|
/**
|
|
353
364
|
* The configuration file, or `None` when this machine has none yet.
|
|
354
365
|
*
|
|
@@ -368,6 +379,8 @@ const read = Effect.gen(function* () {
|
|
|
368
379
|
try: () => Yaml.parse(raw),
|
|
369
380
|
catch: (cause) => malformed(reasonOf(cause))
|
|
370
381
|
})) ?? {};
|
|
382
|
+
const legacy = legacyIn(decided);
|
|
383
|
+
if (legacy !== null) return yield* malformed(legacy);
|
|
371
384
|
return Option.some(yield* Schema.decodeUnknownEffect(ConfigFile)(decided, {
|
|
372
385
|
onExcessProperty: "error",
|
|
373
386
|
errors: "all"
|
|
@@ -381,17 +394,16 @@ const mapping = (entries) => {
|
|
|
381
394
|
const settingsDocument = (patch) => mapping([
|
|
382
395
|
["base", patch.base],
|
|
383
396
|
["review", patch.review === void 0 ? void 0 : mapping([
|
|
384
|
-
["
|
|
397
|
+
["command", patch.review.command],
|
|
385
398
|
["effort", patch.review.effort],
|
|
399
|
+
["prompt", patch.review.prompt],
|
|
386
400
|
["model", patch.review.model],
|
|
387
|
-
["
|
|
388
|
-
["docs_only", patch.review.docs_only],
|
|
389
|
-
["path_instructions", patch.review.path_instructions]
|
|
401
|
+
["docs_only", patch.review.docs_only]
|
|
390
402
|
])],
|
|
391
403
|
["ci", patch.ci === void 0 ? void 0 : mapping([["ignore", patch.ci.ignore], ["flaky_patterns", patch.ci.flaky_patterns]])],
|
|
392
404
|
["fix", patch.fix === void 0 ? void 0 : mapping([["commits", patch.fix.commits]])],
|
|
393
405
|
["rebase", patch.rebase === void 0 ? void 0 : mapping([["enabled", patch.rebase.enabled]])],
|
|
394
|
-
["stamp", patch.stamp === void 0 ? void 0 : mapping([["blocks_on", patch.stamp.blocks_on]
|
|
406
|
+
["stamp", patch.stamp === void 0 ? void 0 : mapping([["blocks_on", patch.stamp.blocks_on]])]
|
|
395
407
|
]);
|
|
396
408
|
/**
|
|
397
409
|
* The file as a YAML document, in the order of the schema.
|
|
@@ -400,11 +412,7 @@ const settingsDocument = (patch) => mapping([
|
|
|
400
412
|
* keeps the file stable across runs, so a rewrite shows only what changed.
|
|
401
413
|
*/
|
|
402
414
|
const fileDocument = (file) => mapping([
|
|
403
|
-
["launcher", file.launcher === void 0 ? void 0 : mapping([
|
|
404
|
-
["command", file.launcher.command],
|
|
405
|
-
["fix_args", file.launcher.fix_args],
|
|
406
|
-
["codex", file.launcher.codex]
|
|
407
|
-
])],
|
|
415
|
+
["launcher", file.launcher === void 0 ? void 0 : mapping([["command", file.launcher.command], ["fix_args", file.launcher.fix_args]])],
|
|
408
416
|
["defaults", file.defaults === void 0 ? void 0 : settingsDocument(file.defaults)],
|
|
409
417
|
["repos", file.repos === void 0 ? void 0 : mapping(Object.entries(file.repos).map(([name, patch]) => [name, settingsDocument(patch)]))]
|
|
410
418
|
]);
|
|
@@ -2032,7 +2040,7 @@ const readyReason = (facts) => {
|
|
|
2032
2040
|
* This is the single place the bucket rules exist. Every tracked PR lands in
|
|
2033
2041
|
* exactly one bucket, so the rules are tried in priority order and the first
|
|
2034
2042
|
* that claims the PR wins: a PR that both needs a review run and has changes
|
|
2035
|
-
* requested is mine to move, not the
|
|
2043
|
+
* requested is mine to move, not the review's.
|
|
2036
2044
|
*
|
|
2037
2045
|
* Ready does not insist on an approval, because a repository that requires no
|
|
2038
2046
|
* reviewer never produces one. What it insists on is that nobody else has been
|
|
@@ -2732,11 +2740,12 @@ const recordConflict = Effect.fn("rebase.recordConflict")(function* (repo, numbe
|
|
|
2732
2740
|
/** Whether a review run found anything at all. */
|
|
2733
2741
|
const Verdict = Schema.Literals(["clean", "findings"]);
|
|
2734
2742
|
/**
|
|
2735
|
-
* Every severity word a
|
|
2743
|
+
* Every severity word a review may answer with.
|
|
2736
2744
|
*
|
|
2737
|
-
* The first three are ours, and the only ones a
|
|
2738
|
-
*
|
|
2739
|
-
* a turn that comes back in them is worth reading rather than throwing
|
|
2745
|
+
* The first three are ours, and the only ones a run is asked for. The rest are
|
|
2746
|
+
* the persona a run with no slash command carries, which grades in its own
|
|
2747
|
+
* words: a turn that comes back in them is worth reading rather than throwing
|
|
2748
|
+
* away.
|
|
2740
2749
|
*/
|
|
2741
2750
|
const Spelling = Schema.Literals([
|
|
2742
2751
|
"error",
|
|
@@ -2809,9 +2818,9 @@ const jsonSchema = JSON.stringify(SchemaRepresentation.toJsonSchemaDocument(Sche
|
|
|
2809
2818
|
/**
|
|
2810
2819
|
* The findings as the Markdown a report is written in.
|
|
2811
2820
|
*
|
|
2812
|
-
* It is what
|
|
2813
|
-
* answers in findings and not in prose, so the report kept beside
|
|
2814
|
-
*
|
|
2821
|
+
* It is what a schema-held run's report says: with a schema in force a run
|
|
2822
|
+
* answers in findings and not in prose, so the report kept beside it is written
|
|
2823
|
+
* from the findings themselves rather than left empty.
|
|
2815
2824
|
*/
|
|
2816
2825
|
const asMarkdown = (found) => found.findings.length === 0 ? "Clean: the run found nothing to report." : found.findings.map((finding) => `- \`${finding.file}:${finding.line}\` ${finding.severity}: ${finding.summary}`).join("\n");
|
|
2817
2826
|
/** Where each severity sits against the others, so the bar can be compared with it. */
|
|
@@ -2842,7 +2851,7 @@ const Outcome = Schema.Union([Schema.TaggedStruct("reported", {
|
|
|
2842
2851
|
findings: Schema.Array(Finding)
|
|
2843
2852
|
}), Schema.TaggedStruct("failed", { detail: Schema.String })]);
|
|
2844
2853
|
/**
|
|
2845
|
-
* One
|
|
2854
|
+
* One review run against a tracked PR at a specific head commit.
|
|
2846
2855
|
*
|
|
2847
2856
|
* It is a schema because a review run outlives the command that started it: the
|
|
2848
2857
|
* state directory is where the next sweep learns that this head has been
|
|
@@ -2853,13 +2862,18 @@ const ReviewRun = Schema.Struct({
|
|
|
2853
2862
|
number: Schema.Int,
|
|
2854
2863
|
/** The head the run covers. A run never vouches for code it did not see. */
|
|
2855
2864
|
head: Schema.String,
|
|
2856
|
-
|
|
2857
|
-
|
|
2865
|
+
/**
|
|
2866
|
+
* The slash command line the run opened on, or null where it opened on the
|
|
2867
|
+
* tool's own prompt. A report found months later says what it was asked, and a
|
|
2868
|
+
* record an earlier version wrote carries no such field and is forgotten.
|
|
2869
|
+
*/
|
|
2870
|
+
command: Schema.NullOr(Schema.String),
|
|
2871
|
+
effort: Schema.NullOr(Effort),
|
|
2858
2872
|
/**
|
|
2859
2873
|
* The agent session the run happened in, or null where it never reached one.
|
|
2860
2874
|
*
|
|
2861
|
-
* A
|
|
2862
|
-
*
|
|
2875
|
+
* A run that would not start or exited before it said anything has no session,
|
|
2876
|
+
* and the run is still recorded: a failure is recorded as what it is.
|
|
2863
2877
|
*/
|
|
2864
2878
|
sessionId: Schema.NullOr(Schema.String),
|
|
2865
2879
|
ranAt: Schema.DateTimeUtcFromString,
|
|
@@ -2868,17 +2882,12 @@ const ReviewRun = Schema.Struct({
|
|
|
2868
2882
|
/** A head as it is read out loud: the seven characters git itself abbreviates to. */
|
|
2869
2883
|
const short = (head) => head.slice(0, 7);
|
|
2870
2884
|
/**
|
|
2871
|
-
* Where a run is kept: one key per head
|
|
2872
|
-
*
|
|
2873
|
-
* before it.
|
|
2874
|
-
*
|
|
2875
|
-
* The runner is in the key because a head can carry two opinions: the review
|
|
2876
|
-
* that is my bar and the second one beside it. Without it the second opinion
|
|
2877
|
-
* would overwrite the first and the stamp would rest on whichever ran last.
|
|
2885
|
+
* Where a run is kept: one key per head, so a run and the code it read cannot
|
|
2886
|
+
* drift apart, and a re-review replaces the run before it.
|
|
2878
2887
|
*/
|
|
2879
|
-
const runKey = (repo, number, head
|
|
2888
|
+
const runKey = (repo, number, head) => `${repo}#${number}@${head}`;
|
|
2880
2889
|
/** Where the run's report is kept: beside the run, as the Markdown it is. */
|
|
2881
|
-
const reportKey = (repo, number, head
|
|
2890
|
+
const reportKey = (repo, number, head) => `${runKey(repo, number, head)}.md`;
|
|
2882
2891
|
/**
|
|
2883
2892
|
* Which head a pull request was last reviewed at: an index beside `runKey` and
|
|
2884
2893
|
* `reportKey` rather than a thing the glossary names.
|
|
@@ -2890,35 +2899,27 @@ const reportKey = (repo, number, head, runner) => `${runKey(repo, number, head,
|
|
|
2890
2899
|
*/
|
|
2891
2900
|
const LastReviewed = Schema.Struct({ head: Schema.String });
|
|
2892
2901
|
/** Where that head is kept. No head is spelled `latest`, so nothing collides. */
|
|
2893
|
-
const latestKey = (repo, number
|
|
2902
|
+
const latestKey = (repo, number) => `${repo}#${number}@latest`;
|
|
2894
2903
|
/**
|
|
2895
|
-
* The
|
|
2896
|
-
* none.
|
|
2904
|
+
* The run at one head, or none where nothing has reviewed it.
|
|
2897
2905
|
*
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2906
|
+
* A head is where the question is asked - the stamp, the bucket and `dw-mc
|
|
2907
|
+
* findings` all ask about one commit - and one read off the disk answers it
|
|
2908
|
+
* without an index to keep in step.
|
|
2900
2909
|
*
|
|
2901
2910
|
* A run this version cannot read is a run another version of this record wrote,
|
|
2902
2911
|
* and the state directory is a cache of work that can be done again: forgetting
|
|
2903
2912
|
* it costs one review, where failing here would cost me the command I asked for.
|
|
2904
2913
|
*/
|
|
2905
|
-
const
|
|
2906
|
-
const heads = yield* storeFor("runs", LastReviewed);
|
|
2907
|
-
const at = yield* Effect.orElseSucceed(heads.get(latestKey(repo, number, runner)), () => Option.none());
|
|
2908
|
-
if (Option.isNone(at)) return Option.none();
|
|
2914
|
+
const runAt = Effect.fn("review.runAt")(function* (repo, number, head) {
|
|
2909
2915
|
const runs = yield* storeFor("runs", ReviewRun);
|
|
2910
|
-
return yield* Effect.orElseSucceed(runs.get(runKey(repo, number,
|
|
2916
|
+
return yield* Effect.orElseSucceed(runs.get(runKey(repo, number, head)), () => Option.none());
|
|
2911
2917
|
});
|
|
2912
|
-
/**
|
|
2913
|
-
|
|
2914
|
-
*
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
* runner. Three reads off the disk answer it without an index to keep in step.
|
|
2918
|
-
*/
|
|
2919
|
-
const runsAt = Effect.fn("review.runsAt")(function* (repo, number, head) {
|
|
2920
|
-
const runs = yield* storeFor("runs", ReviewRun);
|
|
2921
|
-
return (yield* Effect.forEach(runners, (runner) => Effect.orElseSucceed(runs.get(runKey(repo, number, head, runner)), () => Option.none()))).flatMap((run) => Option.isSome(run) ? [run.value] : []);
|
|
2918
|
+
/** The last review run on a pull request, or none where it has had none. */
|
|
2919
|
+
const lastRun = Effect.fn("review.lastRun")(function* (repo, number) {
|
|
2920
|
+
const heads = yield* storeFor("runs", LastReviewed);
|
|
2921
|
+
const at = yield* Effect.orElseSucceed(heads.get(latestKey(repo, number)), () => Option.none());
|
|
2922
|
+
return Option.isNone(at) ? Option.none() : yield* runAt(repo, number, at.value.head);
|
|
2922
2923
|
});
|
|
2923
2924
|
/**
|
|
2924
2925
|
* What a run reported, or null where it reported nothing at all.
|
|
@@ -2964,73 +2965,38 @@ const skippedSince = (asked, docsOnly) => {
|
|
|
2964
2965
|
const changed = asked.last.head === asked.head ? [] : asked.changed;
|
|
2965
2966
|
return changed === null || worthRerunning(changed, docsOnly) ? null : asked.last.head;
|
|
2966
2967
|
};
|
|
2968
|
+
/** What a run was opened on, as the report says it. */
|
|
2969
|
+
const askedOf$1 = (run) => run.command === null ? "the tool's own prompt" : [run.command, run.effort].filter((part) => part !== null).join(" ");
|
|
2967
2970
|
/**
|
|
2968
|
-
* The report as it is written down: what it is of, then what the
|
|
2971
|
+
* The report as it is written down: what it is of, then what the run said.
|
|
2969
2972
|
*
|
|
2970
2973
|
* The heading is the whole point of writing it rather than storing the prose
|
|
2971
2974
|
* alone - a file found months later says which pull request, which commit and
|
|
2972
|
-
*
|
|
2975
|
+
* what the run was asked, without anything else having to be open.
|
|
2973
2976
|
*/
|
|
2974
2977
|
const reportDocument = (run, title, prose) => [
|
|
2975
2978
|
`# ${run.repo}#${run.number} ${title}`,
|
|
2976
2979
|
"",
|
|
2977
2980
|
`- head: ${run.head}`,
|
|
2978
|
-
`-
|
|
2981
|
+
`- run: ${askedOf$1(run)}`,
|
|
2979
2982
|
`- ran: ${DateTime.formatIso(run.ranAt)}`,
|
|
2980
2983
|
"",
|
|
2981
2984
|
prose.trim(),
|
|
2982
2985
|
""
|
|
2983
2986
|
].join("\n");
|
|
2984
2987
|
/**
|
|
2985
|
-
*
|
|
2986
|
-
* without repeats.
|
|
2987
|
-
*
|
|
2988
|
-
* The order is the precedence everything downstream reads: it is the order the
|
|
2989
|
-
* runners run in, and the first of them is the run `dw-mc findings` and a fix
|
|
2990
|
-
* session reach for when I do not name one.
|
|
2991
|
-
*/
|
|
2992
|
-
const runnersFor = (configured) => [...new Set(configured)];
|
|
2993
|
-
/** Which agent CLI a runner reaches. `builtin` and `prompt` are Claude Code's. */
|
|
2994
|
-
const onCodex = (runner) => runner === "codex";
|
|
2995
|
-
/**
|
|
2996
|
-
* The configured runners that are a second opinion rather than my bar.
|
|
2997
|
-
*
|
|
2998
|
-
* Codex is supporting because something else is the review: a machine that
|
|
2999
|
-
* configured Codex alone did not ask for a second opinion, it asked for a
|
|
3000
|
-
* review, and nothing there could ever earn a stamp if this called it
|
|
3001
|
-
* supporting.
|
|
3002
|
-
*/
|
|
3003
|
-
const supportingIn = (configured) => {
|
|
3004
|
-
const all = runnersFor(configured);
|
|
3005
|
-
return all.some((runner) => !onCodex(runner)) ? all.filter(onCodex) : [];
|
|
3006
|
-
};
|
|
3007
|
-
/**
|
|
3008
|
-
* The configured runners whose findings decide the stamp.
|
|
3009
|
-
*
|
|
3010
|
-
* A second opinion informs me without gating my bar, which is what
|
|
3011
|
-
* `stamp.supporting_blocks` turns off: set, every configured runner decides.
|
|
3012
|
-
*/
|
|
3013
|
-
const decidingIn = (configured, supportingBlocks) => {
|
|
3014
|
-
const all = runnersFor(configured);
|
|
3015
|
-
if (supportingBlocks) return all;
|
|
3016
|
-
const supporting = supportingIn(configured);
|
|
3017
|
-
return all.filter((runner) => !supporting.includes(runner));
|
|
3018
|
-
};
|
|
3019
|
-
/**
|
|
3020
|
-
* Whether `head` has the review it needs: every deciding runner has reported on
|
|
3021
|
-
* it.
|
|
2988
|
+
* Whether `head` has the review it needs.
|
|
3022
2989
|
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
* nothing
|
|
3026
|
-
* else: a failure has found nothing, not found nothing wrong.
|
|
2990
|
+
* A run that reported nothing does not count, which is the same rule
|
|
2991
|
+
* `reportedBy` draws everywhere else: a failure has found nothing, not found
|
|
2992
|
+
* nothing wrong.
|
|
3027
2993
|
*/
|
|
3028
|
-
const reviewedBy = (
|
|
3029
|
-
/** The findings at one head that withhold the stamp
|
|
3030
|
-
const blockingIn = (
|
|
3031
|
-
const found = reportedBy(run);
|
|
2994
|
+
const reviewedBy = (run) => run !== null && reportedBy(run) !== null;
|
|
2995
|
+
/** The findings at one head that withhold the stamp. */
|
|
2996
|
+
const blockingIn = (run, blocksOn) => {
|
|
2997
|
+
const found = run === null ? null : reportedBy(run);
|
|
3032
2998
|
return found === null ? [] : blocking(found.findings, blocksOn);
|
|
3033
|
-
}
|
|
2999
|
+
};
|
|
3034
3000
|
/**
|
|
3035
3001
|
* What the review runs on `head` say about it, for the stamp to rest on.
|
|
3036
3002
|
*
|
|
@@ -3040,20 +3006,15 @@ const blockingIn = (runs, deciding, blocksOn) => runs.filter((run) => deciding.i
|
|
|
3040
3006
|
* report findings does not count either: its verdict is what takes a pull
|
|
3041
3007
|
* request out of Needs review run, and it reached none.
|
|
3042
3008
|
*
|
|
3043
|
-
* A head may carry a run from each configured runner, and it is reviewed once
|
|
3044
|
-
* every runner that decides my bar has reported on it. A second opinion's
|
|
3045
|
-
* findings are read here only where `stamp.supporting_blocks` lets them block.
|
|
3046
|
-
*
|
|
3047
3009
|
* It is one function because the two callers are a sweep and `dw-mc merge`, and
|
|
3048
3010
|
* the second exists to land what the first only describes: two spellings of
|
|
3049
3011
|
* this would be two answers to whether a head has been reviewed.
|
|
3050
3012
|
*/
|
|
3051
3013
|
const reviewedAt = Effect.fn("review.reviewedAt")(function* (repo, number, head, settings) {
|
|
3052
|
-
const
|
|
3053
|
-
const runs = yield* runsAt(repo, number, head);
|
|
3014
|
+
const run = Option.getOrNull(yield* runAt(repo, number, head));
|
|
3054
3015
|
return {
|
|
3055
|
-
reviewRunHead: reviewedBy(
|
|
3056
|
-
blockingFindings: blockingIn(
|
|
3016
|
+
reviewRunHead: reviewedBy(run) ? head : null,
|
|
3017
|
+
blockingFindings: blockingIn(run, settings.stamp.blocks_on).length
|
|
3057
3018
|
};
|
|
3058
3019
|
});
|
|
3059
3020
|
//#endregion
|
|
@@ -3247,9 +3208,8 @@ const separated = (blocks) => blocks.flatMap((lines, index) => index === 0 ? lin
|
|
|
3247
3208
|
* The conversation on screen: people first, then a rule, then the bots.
|
|
3248
3209
|
*
|
|
3249
3210
|
* The rule is there so the two are never read as one list. A bot's comment is
|
|
3250
|
-
* observed and never answered,
|
|
3251
|
-
*
|
|
3252
|
-
* ignore bots for exactly this reason.
|
|
3211
|
+
* observed and never answered, and the bucket rules ignore bots for exactly
|
|
3212
|
+
* this reason.
|
|
3253
3213
|
*
|
|
3254
3214
|
* A bot is cut at the same moment I am measured against, because the window is
|
|
3255
3215
|
* what has happened since I last acted rather than what is owed an answer. A
|
|
@@ -3312,7 +3272,6 @@ const comments = Command.make("comments", {
|
|
|
3312
3272
|
//#region src/cli/findings.ts
|
|
3313
3273
|
/** The findings as the JSON the schema defines, rather than as this file spells it. */
|
|
3314
3274
|
const asJson$2 = Schema.encodeEffect(Schema.fromJsonString(Findings));
|
|
3315
|
-
const runnerFlag$1 = Flag.Literals("runner", [...runners]).pipe(Flag.withDescription("Which runner's findings to print, over the first the repository configured"), Flag.optional);
|
|
3316
3275
|
const jsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print the findings as the JSON a fix session is handed"));
|
|
3317
3276
|
/** What a run's findings come to in one line, against the bar that blocks. */
|
|
3318
3277
|
const summary = (found, blocksOn) => {
|
|
@@ -3321,10 +3280,10 @@ const summary = (found, blocksOn) => {
|
|
|
3321
3280
|
return `${count(found.findings.length, "finding")}, ${blocked} blocking`;
|
|
3322
3281
|
};
|
|
3323
3282
|
/** Which run these findings are, and what they come to: the line above the list. */
|
|
3324
|
-
const header$1 = (run, found, blocksOn) => `${run.repo}#${run.number} ${short(run.head)} ${
|
|
3283
|
+
const header$1 = (run, found, blocksOn) => `${run.repo}#${run.number} ${short(run.head)} ${summary(found, blocksOn)}`;
|
|
3325
3284
|
/**
|
|
3326
|
-
* The findings one to a line, in the order the
|
|
3327
|
-
*
|
|
3285
|
+
* The findings one to a line, in the order the run reported them, ruled so the
|
|
3286
|
+
* three columns read apart.
|
|
3328
3287
|
*/
|
|
3329
3288
|
const lines$1 = (found) => table(found.findings.map((finding) => [
|
|
3330
3289
|
`${finding.file}:${finding.line}`,
|
|
@@ -3339,17 +3298,10 @@ const lines$1 = (found) => table(found.findings.map((finding) => [
|
|
|
3339
3298
|
* off the state directory rather than worked out from GitHub: this command is
|
|
3340
3299
|
* one I run inside a fix session, where another round trip to GitHub buys
|
|
3341
3300
|
* nothing the run it is about to fix does not already say.
|
|
3342
|
-
*
|
|
3343
|
-
* Which run, where a head carries more than one, is the first of `runners` that
|
|
3344
|
-
* has one - the order the repository names them in, unless I name one myself.
|
|
3345
|
-
* A second opinion is worth reading and is not what I fix by default.
|
|
3346
3301
|
*/
|
|
3347
|
-
const currentRun = Effect.fn("findings.currentRun")(function* (repo, number
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
if (Option.isSome(run)) return run.value;
|
|
3351
|
-
}
|
|
3352
|
-
return yield* asUserError(`No review run on ${repo}#${number}. Run dw-mc review ${number} first.`);
|
|
3302
|
+
const currentRun = Effect.fn("findings.currentRun")(function* (repo, number) {
|
|
3303
|
+
const run = yield* lastRun(repo, number);
|
|
3304
|
+
return Option.isSome(run) ? run.value : yield* asUserError(`No review run on ${repo}#${number}. Run dw-mc review ${number} first.`);
|
|
3353
3305
|
});
|
|
3354
3306
|
/**
|
|
3355
3307
|
* What the run reported, or the sentence saying it reported nothing at all.
|
|
@@ -3374,16 +3326,12 @@ const whatItFound = (run) => {
|
|
|
3374
3326
|
*/
|
|
3375
3327
|
const findings = Command.make("findings", {
|
|
3376
3328
|
pr: prArgument,
|
|
3377
|
-
runner: runnerFlag$1,
|
|
3378
3329
|
json: jsonFlag
|
|
3379
|
-
}, Effect.fn("findings")(function* ({ json, pr
|
|
3330
|
+
}, Effect.fn("findings")(function* ({ json, pr }) {
|
|
3380
3331
|
const file = Option.getOrElse(yield* read, () => ({}));
|
|
3381
3332
|
const { number, repo } = yield* named(pr, Object.keys(file.repos ?? {}).toSorted());
|
|
3382
3333
|
const settings = settingsFor(file, repo);
|
|
3383
|
-
const run = yield* currentRun(repo, number
|
|
3384
|
-
onNone: () => settings.review.runners,
|
|
3385
|
-
onSome: (only) => [only]
|
|
3386
|
-
}));
|
|
3334
|
+
const run = yield* currentRun(repo, number);
|
|
3387
3335
|
const found = yield* whatItFound(run);
|
|
3388
3336
|
if (json) {
|
|
3389
3337
|
yield* Console.log(yield* asJson$2(found));
|
|
@@ -3395,19 +3343,20 @@ const findings = Command.make("findings", {
|
|
|
3395
3343
|
//#endregion
|
|
3396
3344
|
//#region src/adapters/agent.ts
|
|
3397
3345
|
/**
|
|
3398
|
-
* How one turn of
|
|
3399
|
-
*
|
|
3346
|
+
* How one turn of Claude Code is spawned and given up on, and what a turn that
|
|
3347
|
+
* answers against a schema comes back with.
|
|
3400
3348
|
*
|
|
3401
|
-
*
|
|
3402
|
-
*
|
|
3349
|
+
* Every turn is reached this way, so the spawn, the patience and the one failure
|
|
3350
|
+
* they can end in live here rather than once per turn.
|
|
3403
3351
|
*/
|
|
3404
3352
|
/** A review run that would not start, would not finish, or finished badly. */
|
|
3405
|
-
var
|
|
3406
|
-
|
|
3353
|
+
var AgentFailed = class extends Schema.TaggedError()("AgentFailed", {
|
|
3354
|
+
/** The program that was spawned, which is what a search for it has to name. */
|
|
3355
|
+
program: Schema.String,
|
|
3407
3356
|
detail: Schema.String
|
|
3408
3357
|
}) {
|
|
3409
3358
|
get message() {
|
|
3410
|
-
return `The ${this.
|
|
3359
|
+
return `The ${this.program} review run failed: ${this.detail}`;
|
|
3411
3360
|
}
|
|
3412
3361
|
};
|
|
3413
3362
|
/**
|
|
@@ -3417,15 +3366,15 @@ var RunnerFailed = class extends Schema.TaggedError()("RunnerFailed", {
|
|
|
3417
3366
|
* program that would not start or exited badly, and saying `claude` sends the
|
|
3418
3367
|
* search to the wrong process.
|
|
3419
3368
|
*/
|
|
3420
|
-
const failedBy = (program) => (detail) => new
|
|
3421
|
-
|
|
3369
|
+
const failedBy = (program) => (detail) => new AgentFailed({
|
|
3370
|
+
program,
|
|
3422
3371
|
detail
|
|
3423
3372
|
});
|
|
3424
3373
|
/**
|
|
3425
3374
|
* How long each turn gets before it is given up on.
|
|
3426
3375
|
*
|
|
3427
3376
|
* The review is the turn that thinks, and a high-effort one that fans out to
|
|
3428
|
-
* subagents takes real minutes, so its limit is there to catch a
|
|
3377
|
+
* subagents takes real minutes, so its limit is there to catch a run that has
|
|
3429
3378
|
* stopped rather than one that is slow. The second turn reads no code and
|
|
3430
3379
|
* decides nothing - the review it reports on is already in the session it
|
|
3431
3380
|
* resumes - and every run of it by hand came back in seconds.
|
|
@@ -3442,9 +3391,9 @@ const patience = {
|
|
|
3442
3391
|
*
|
|
3443
3392
|
* The launcher's own arguments go in front of the turn's, because they are what
|
|
3444
3393
|
* gets `claude` started at all. The two output streams are drained together,
|
|
3445
|
-
* because draining one to the end first can block a
|
|
3446
|
-
*
|
|
3447
|
-
* `
|
|
3394
|
+
* because draining one to the end first can block a run that is still writing to
|
|
3395
|
+
* the other. Every way a turn can fail to finish comes back from here as a
|
|
3396
|
+
* `AgentFailed`, so a caller is left with the turn's own answer and nothing else
|
|
3448
3397
|
* to translate - a turn that never comes back included.
|
|
3449
3398
|
*/
|
|
3450
3399
|
const turn = Effect.fnUntraced(function* (options) {
|
|
@@ -3454,7 +3403,7 @@ const turn = Effect.fnUntraced(function* (options) {
|
|
|
3454
3403
|
const running = Effect.gen(function* () {
|
|
3455
3404
|
const handle = yield* Effect.mapError(spawner.spawn(ChildProcess.make(program, [...prefix, ...options.args], {
|
|
3456
3405
|
cwd: options.directory,
|
|
3457
|
-
stdin:
|
|
3406
|
+
stdin: "pipe"
|
|
3458
3407
|
})), (error) => failed(error.message));
|
|
3459
3408
|
const [got, stderr] = yield* Effect.mapError(Effect.all([options.read(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], { concurrency: 2 }), (error) => failed(error.message));
|
|
3460
3409
|
const exitCode = yield* Effect.mapError(handle.exitCode, (error) => failed(error.message));
|
|
@@ -3467,118 +3416,10 @@ const turn = Effect.fnUntraced(function* (options) {
|
|
|
3467
3416
|
});
|
|
3468
3417
|
});
|
|
3469
3418
|
//#endregion
|
|
3470
|
-
//#region src/adapters/
|
|
3471
|
-
/**
|
|
3472
|
-
* The events of a `codex exec --json` run this reads, as it really writes them.
|
|
3473
|
-
*
|
|
3474
|
-
* Every other field and every other event is ignored, the same way the Claude
|
|
3475
|
-
* Code transcript is read: a run carries reasoning, tool results and a usage
|
|
3476
|
-
* report, and a Codex version that adds another event must not stop a run from
|
|
3477
|
-
* being read.
|
|
3478
|
-
*/
|
|
3479
|
-
const CodexEvent = Schema.Struct({
|
|
3480
|
-
type: Schema.String,
|
|
3481
|
-
/** On `thread.started`: the session the run happens in. */
|
|
3482
|
-
thread_id: Schema.optionalKey(Schema.String),
|
|
3483
|
-
item: Schema.optionalKey(Schema.Struct({
|
|
3484
|
-
type: Schema.String,
|
|
3485
|
-
/** On an `agent_message`: what the run answered, which the schema makes JSON. */
|
|
3486
|
-
text: Schema.optionalKey(Schema.String)
|
|
3487
|
-
}))
|
|
3488
|
-
});
|
|
3489
|
-
const asCodexEvent = Schema.decodeUnknownOption(Schema.fromJsonString(CodexEvent));
|
|
3490
|
-
/**
|
|
3491
|
-
* What Codex reached for, as the item types it reports it in.
|
|
3492
|
-
*
|
|
3493
|
-
* A run says it is alive by what it does, and what Codex does is run shell
|
|
3494
|
-
* commands, change files and call tools. Its own words for those are what the
|
|
3495
|
-
* progress line prints, so nothing here has to pretend Codex is Claude Code.
|
|
3496
|
-
*/
|
|
3497
|
-
const codexTool = {
|
|
3498
|
-
command_execution: "shell",
|
|
3499
|
-
file_change: "edit",
|
|
3500
|
-
mcp_tool_call: "tool",
|
|
3501
|
-
web_search: "search"
|
|
3502
|
-
};
|
|
3503
|
-
/**
|
|
3504
|
-
* One review run of the same prompt on the Codex CLI, in `directory`.
|
|
3505
|
-
*
|
|
3506
|
-
* This is the second opinion, so it answers the same schema and is recorded as
|
|
3507
|
-
* the same review run: everything downstream - the stamp, the buckets, `dw-mc
|
|
3508
|
-
* findings`, a fix session - cannot tell which CLI read the code.
|
|
3509
|
-
*
|
|
3510
|
-
* Codex takes its schema as a file and never inline, which is the mirror image
|
|
3511
|
-
* of Claude Code, so the schema is written to a temporary file that lives as
|
|
3512
|
-
* long as the run. Standard input is closed because `codex exec` reads it to
|
|
3513
|
-
* the end and appends it to the prompt. The sandbox is read-only: a review
|
|
3514
|
-
* reads, and a review run of mine has no business writing in the worktree it
|
|
3515
|
-
* was cut into.
|
|
3516
|
-
*
|
|
3517
|
-
* There is no prose: with a schema in force every message Codex sends is the
|
|
3518
|
-
* JSON the schema describes, so what it found is all there is, and the report
|
|
3519
|
-
* kept beside the run is written from the findings themselves.
|
|
3520
|
-
*/
|
|
3521
|
-
const codexReview = Effect.fn("runner.codexReview")(function* (options) {
|
|
3522
|
-
const [program] = options.launcher.codex;
|
|
3523
|
-
const failed = failedBy(program);
|
|
3524
|
-
const fs = yield* FileSystem.FileSystem;
|
|
3525
|
-
const schemaFile = yield* Effect.mapError(fs.makeTempFileScoped({
|
|
3526
|
-
prefix: "dw-mc-findings-",
|
|
3527
|
-
suffix: ".json"
|
|
3528
|
-
}), (error) => failed(error.message));
|
|
3529
|
-
yield* Effect.mapError(fs.writeFileString(schemaFile, options.jsonSchema), (error) => failed(error.message));
|
|
3530
|
-
const run = yield* turn({
|
|
3531
|
-
command: options.launcher.codex,
|
|
3532
|
-
directory: options.directory,
|
|
3533
|
-
patience: {
|
|
3534
|
-
turn: "the review",
|
|
3535
|
-
duration: patience.reviewing
|
|
3536
|
-
},
|
|
3537
|
-
stdin: "ignore",
|
|
3538
|
-
args: [
|
|
3539
|
-
"exec",
|
|
3540
|
-
"--json",
|
|
3541
|
-
"--output-schema",
|
|
3542
|
-
schemaFile,
|
|
3543
|
-
"--sandbox",
|
|
3544
|
-
"read-only",
|
|
3545
|
-
...options.model === null ? [] : ["--model", options.model],
|
|
3546
|
-
options.prompt
|
|
3547
|
-
],
|
|
3548
|
-
read: (stdout) => stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.mapEffect((line) => {
|
|
3549
|
-
const event = asCodexEvent(line);
|
|
3550
|
-
const reached = Option.isSome(event) && event.value.type === "item.started" ? codexTool[event.value.item?.type ?? ""] : void 0;
|
|
3551
|
-
return Effect.as(reached === void 0 ? Effect.void : options.onTool(reached), event);
|
|
3552
|
-
}), Stream.runFold(() => ({
|
|
3553
|
-
thread: Option.none(),
|
|
3554
|
-
said: []
|
|
3555
|
-
}), (soFar, event) => {
|
|
3556
|
-
if (Option.isNone(event)) return soFar;
|
|
3557
|
-
const { item, thread_id, type } = event.value;
|
|
3558
|
-
const said = type === "item.completed" && item?.type === "agent_message" && item.text !== void 0 ? [...soFar.said, item.text] : soFar.said;
|
|
3559
|
-
return {
|
|
3560
|
-
thread: Option.orElse(Option.fromUndefinedOr(thread_id), () => soFar.thread),
|
|
3561
|
-
said
|
|
3562
|
-
};
|
|
3563
|
-
}))
|
|
3564
|
-
});
|
|
3565
|
-
if (Option.isNone(run.thread)) return yield* failed("the run never said which thread it was in");
|
|
3566
|
-
const answer = run.said.at(-1);
|
|
3567
|
-
if (answer === void 0) return yield* failed("the run came back with no structured output");
|
|
3568
|
-
return {
|
|
3569
|
-
findings: yield* Effect.try({
|
|
3570
|
-
try: () => JSON.parse(answer),
|
|
3571
|
-
catch: () => failed("the run answered in something that is not JSON")
|
|
3572
|
-
}),
|
|
3573
|
-
sessionId: run.thread.value,
|
|
3574
|
-
prose: null
|
|
3575
|
-
};
|
|
3576
|
-
}, Effect.scoped);
|
|
3577
|
-
//#endregion
|
|
3578
|
-
//#region src/adapters/runner.ts
|
|
3419
|
+
//#region src/adapters/claude.ts
|
|
3579
3420
|
/**
|
|
3580
|
-
* Claude Code
|
|
3581
|
-
* the sessions I steer.
|
|
3421
|
+
* Claude Code: a review on a slash command, a review on the tool's own prompt,
|
|
3422
|
+
* and the sessions I steer.
|
|
3582
3423
|
*/
|
|
3583
3424
|
/**
|
|
3584
3425
|
* The two events of a stream-json run this reads, as the runner really writes
|
|
@@ -3594,7 +3435,7 @@ const Working = Schema.Struct({
|
|
|
3594
3435
|
text: Schema.optionalKey(Schema.String)
|
|
3595
3436
|
})) })
|
|
3596
3437
|
});
|
|
3597
|
-
const
|
|
3438
|
+
const Ended = Schema.Struct({
|
|
3598
3439
|
type: Schema.Literal("result"),
|
|
3599
3440
|
subtype: Schema.String,
|
|
3600
3441
|
is_error: Schema.Boolean,
|
|
@@ -3604,7 +3445,7 @@ const Result$1 = Schema.Struct({
|
|
|
3604
3445
|
structured_output: Schema.optionalKey(Schema.Unknown)
|
|
3605
3446
|
});
|
|
3606
3447
|
const asWorking = Schema.decodeUnknownOption(Schema.fromJsonString(Working));
|
|
3607
|
-
const asResult = Schema.decodeUnknownOption(Schema.fromJsonString(
|
|
3448
|
+
const asResult = Schema.decodeUnknownOption(Schema.fromJsonString(Ended));
|
|
3608
3449
|
const heardIn = (line) => {
|
|
3609
3450
|
const blocks = Option.match(asWorking(line), {
|
|
3610
3451
|
onNone: () => [],
|
|
@@ -3618,7 +3459,7 @@ const heardIn = (line) => {
|
|
|
3618
3459
|
/**
|
|
3619
3460
|
* The result a turn ended on, or the failure it really was.
|
|
3620
3461
|
*
|
|
3621
|
-
* A turn that said nothing this can read and a turn
|
|
3462
|
+
* A turn that said nothing this can read and a turn Claude Code itself calls an
|
|
3622
3463
|
* error are both failures: `subtype` is where a run that hit its turn limit or
|
|
3623
3464
|
* lost its connection says so, and its `result` is the only word on why.
|
|
3624
3465
|
*/
|
|
@@ -3633,8 +3474,8 @@ const ended = (program, result) => {
|
|
|
3633
3474
|
* to `onTool` while the run is still going, and what it said and how it ended
|
|
3634
3475
|
* are what comes back.
|
|
3635
3476
|
*
|
|
3636
|
-
* Both of
|
|
3637
|
-
*
|
|
3477
|
+
* Both shapes of review read a turn the same way, so the fold is here rather
|
|
3478
|
+
* than once per shape.
|
|
3638
3479
|
*/
|
|
3639
3480
|
const transcript = (onTool) => (stdout) => stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.mapEffect((line) => {
|
|
3640
3481
|
const heard = heardIn(line);
|
|
@@ -3650,30 +3491,38 @@ const transcript = (onTool) => (stdout) => stdout.pipe(Stream.decodeText(), Stre
|
|
|
3650
3491
|
result: Option.orElse(asResult(line), () => soFar.result)
|
|
3651
3492
|
})));
|
|
3652
3493
|
/**
|
|
3653
|
-
* One review run
|
|
3494
|
+
* One review run on a slash command, headless, in `directory`.
|
|
3654
3495
|
*
|
|
3655
3496
|
* The run is in the foreground and says what it is doing as it does it, which
|
|
3656
3497
|
* is what `onTool` is for: a review takes minutes, and a terminal that prints
|
|
3657
3498
|
* nothing for minutes is one I stop trusting.
|
|
3658
3499
|
*
|
|
3500
|
+
* `--json-schema` is never passed here: verified by running it, the flag beside
|
|
3501
|
+
* `/code-review` breaks the run, which is why a slash command costs a second
|
|
3502
|
+
* turn that resumes the session and asks for the findings. My own instructions
|
|
3503
|
+
* ride on `--append-system-prompt` rather than on the command's own line,
|
|
3504
|
+
* because what a slash command does with its arguments is its business and not
|
|
3505
|
+
* this tool's.
|
|
3506
|
+
*
|
|
3659
3507
|
* `--comment` is the flag that makes the built-in review post on the pull
|
|
3660
|
-
* request, and it is never passed (ADR 0002). The report is everything
|
|
3661
|
-
*
|
|
3662
|
-
* running it, a repository whose review command fans out to subagents can end
|
|
3663
|
-
*
|
|
3664
|
-
* turn is what turns the prose into findings, and it needs this run's session.
|
|
3508
|
+
* request, and it is never passed either (ADR 0002). The report is everything
|
|
3509
|
+
* the run said on its own turns rather than the `result` alone: verified by
|
|
3510
|
+
* running it, a repository whose review command fans out to subagents can end on
|
|
3511
|
+
* a remark about them, and the report is the turn before that.
|
|
3665
3512
|
*/
|
|
3666
|
-
const
|
|
3513
|
+
const commandReview = Effect.fn("claude.commandReview")(function* (options) {
|
|
3667
3514
|
const [program] = options.launcher.command;
|
|
3668
3515
|
const run = yield* turn({
|
|
3669
3516
|
command: options.launcher.command,
|
|
3670
3517
|
directory: options.directory,
|
|
3671
3518
|
args: [
|
|
3672
3519
|
"-p",
|
|
3673
|
-
|
|
3520
|
+
options.line,
|
|
3674
3521
|
"--output-format",
|
|
3675
3522
|
"stream-json",
|
|
3676
|
-
"--verbose"
|
|
3523
|
+
"--verbose",
|
|
3524
|
+
...options.instructions === null ? [] : ["--append-system-prompt", options.instructions],
|
|
3525
|
+
...options.model === null ? [] : ["--model", options.model]
|
|
3677
3526
|
],
|
|
3678
3527
|
patience: {
|
|
3679
3528
|
turn: "the review",
|
|
@@ -3712,12 +3561,12 @@ const reportFindings = [
|
|
|
3712
3561
|
* is what makes it cheap and what makes it accurate - verified by running it,
|
|
3713
3562
|
* the line numbers it reports beat the ones the prose gives. The output is
|
|
3714
3563
|
* handed on as it arrived: what the findings must look like belongs to the
|
|
3715
|
-
* domain, and the schema the
|
|
3564
|
+
* domain, and the schema the run is held to comes in from there too.
|
|
3716
3565
|
*
|
|
3717
|
-
* Every way this can end badly ends as
|
|
3566
|
+
* Every way this can end badly ends as an `AgentFailed`, because a review run
|
|
3718
3567
|
* that could not report is a failure and never a clean verdict.
|
|
3719
3568
|
*/
|
|
3720
|
-
const
|
|
3569
|
+
const findingsTurn = Effect.fn("claude.findingsTurn")(function* (options) {
|
|
3721
3570
|
const [program] = options.launcher.command;
|
|
3722
3571
|
const printed = yield* turn({
|
|
3723
3572
|
command: options.launcher.command,
|
|
@@ -3743,19 +3592,15 @@ const builtinFindings = Effect.fn("runner.builtinFindings")(function* (options)
|
|
|
3743
3592
|
return structured_output;
|
|
3744
3593
|
}, Effect.scoped);
|
|
3745
3594
|
/**
|
|
3746
|
-
* One review run of the tool's own review prompt
|
|
3747
|
-
*
|
|
3748
|
-
* It is one turn rather than two: verified by running it, `--json-schema`
|
|
3749
|
-
* beside an ordinary prompt gives both the prose the run wrote and the
|
|
3750
|
-
* `structured_output` it validated, where the same flag on the built-in
|
|
3751
|
-
* `/code-review` breaks the run. The schema arrives as inline JSON and never as
|
|
3752
|
-
* a path - a path is where Claude Code reports `--json-schema is not valid
|
|
3753
|
-
* JSON`.
|
|
3595
|
+
* One review run of the tool's own review prompt, in `directory`.
|
|
3754
3596
|
*
|
|
3755
|
-
*
|
|
3756
|
-
*
|
|
3597
|
+
* It is one turn rather than two: verified by running it, `--json-schema` beside
|
|
3598
|
+
* an ordinary prompt gives both the prose the run wrote and the
|
|
3599
|
+
* `structured_output` it validated, where the same flag on a slash command
|
|
3600
|
+
* breaks the run. The schema arrives as inline JSON and never as a path - a path
|
|
3601
|
+
* is where Claude Code reports `--json-schema is not valid JSON`.
|
|
3757
3602
|
*/
|
|
3758
|
-
const promptReview = Effect.fn("
|
|
3603
|
+
const promptReview = Effect.fn("claude.promptReview")(function* (options) {
|
|
3759
3604
|
const [program] = options.launcher.command;
|
|
3760
3605
|
const run = yield* turn({
|
|
3761
3606
|
command: options.launcher.command,
|
|
@@ -3786,13 +3631,53 @@ const promptReview = Effect.fn("runner.promptReview")(function* (options) {
|
|
|
3786
3631
|
};
|
|
3787
3632
|
}, Effect.scoped);
|
|
3788
3633
|
/**
|
|
3789
|
-
*
|
|
3634
|
+
* One review run, in whichever shape it was configured in.
|
|
3635
|
+
*
|
|
3636
|
+
* A slash command takes two turns and the tool's own prompt takes one, which is
|
|
3637
|
+
* Claude Code's doing and nobody else's: a caller hands over the turn and gets
|
|
3638
|
+
* the same answer back either way.
|
|
3790
3639
|
*
|
|
3791
|
-
* The
|
|
3792
|
-
*
|
|
3793
|
-
*
|
|
3640
|
+
* The second turn's failure is kept beside the first turn's prose rather than
|
|
3641
|
+
* replacing it. A review that ran and could not report is still worth reading,
|
|
3642
|
+
* and it is recorded as the failure it is.
|
|
3794
3643
|
*/
|
|
3795
|
-
const
|
|
3644
|
+
const reviewTurns = Effect.fn("claude.reviewTurns")(function* (options) {
|
|
3645
|
+
const { directory, jsonSchema, launcher, model, onTool } = options;
|
|
3646
|
+
if (options.turn._tag === "prompt") {
|
|
3647
|
+
const run = yield* promptReview({
|
|
3648
|
+
launcher,
|
|
3649
|
+
directory,
|
|
3650
|
+
prompt: options.turn.text,
|
|
3651
|
+
model,
|
|
3652
|
+
jsonSchema,
|
|
3653
|
+
onTool
|
|
3654
|
+
});
|
|
3655
|
+
return {
|
|
3656
|
+
sessionId: run.sessionId,
|
|
3657
|
+
prose: run.prose,
|
|
3658
|
+
findings: Result.succeed(run.findings)
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
const run = yield* commandReview({
|
|
3662
|
+
launcher,
|
|
3663
|
+
directory,
|
|
3664
|
+
line: options.turn.line,
|
|
3665
|
+
instructions: options.turn.instructions,
|
|
3666
|
+
model,
|
|
3667
|
+
onTool
|
|
3668
|
+
});
|
|
3669
|
+
const findings = yield* Effect.result(findingsTurn({
|
|
3670
|
+
launcher,
|
|
3671
|
+
directory,
|
|
3672
|
+
sessionId: run.sessionId,
|
|
3673
|
+
jsonSchema
|
|
3674
|
+
}));
|
|
3675
|
+
return {
|
|
3676
|
+
sessionId: run.sessionId,
|
|
3677
|
+
prose: run.report,
|
|
3678
|
+
findings
|
|
3679
|
+
};
|
|
3680
|
+
});
|
|
3796
3681
|
/**
|
|
3797
3682
|
* An interactive `claude` in `directory`, opened on `prompt`, with my terminal
|
|
3798
3683
|
* handed straight to it.
|
|
@@ -3802,7 +3687,7 @@ const promptRun = (options) => options.runner === "codex" ? codexReview(options)
|
|
|
3802
3687
|
* no headless review turn wants. They sit in front of the
|
|
3803
3688
|
* prompt, because `claude` takes its flags before its positional argument.
|
|
3804
3689
|
*
|
|
3805
|
-
* This is the one place a
|
|
3690
|
+
* This is the one place a run is not read: the three streams are inherited,
|
|
3806
3691
|
* so what is on the screen is the session itself and not a transcript of it,
|
|
3807
3692
|
* and what I type reaches it. The child is not detached for the same reason -
|
|
3808
3693
|
* a detached child sits outside the terminal's foreground process group, where
|
|
@@ -3815,7 +3700,7 @@ const promptRun = (options) => options.runner === "codex" ? codexReview(options)
|
|
|
3815
3700
|
* Ctrl-C ended badly for `claude` and not for me, so this reports it rather
|
|
3816
3701
|
* than failing on it; only a `claude` that would not start at all is a failure.
|
|
3817
3702
|
*/
|
|
3818
|
-
const steeredSession = Effect.fn("
|
|
3703
|
+
const steeredSession = Effect.fn("claude.steeredSession")(function* (options) {
|
|
3819
3704
|
const [program, ...prefix] = options.launcher.command;
|
|
3820
3705
|
const failed = failedBy(program);
|
|
3821
3706
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -3860,7 +3745,7 @@ const asJson$1 = Schema.encodeEffect(Schema.fromJsonString(Selection));
|
|
|
3860
3745
|
*
|
|
3861
3746
|
* The findings go in verbatim rather than described, because a re-description
|
|
3862
3747
|
* is where a file, a line or my own note quietly changes. A note outranks the
|
|
3863
|
-
* finding it is on: the finding is what
|
|
3748
|
+
* finding it is on: the finding is what the review thought, the note is what I
|
|
3864
3749
|
* think, and I am the one who picked it.
|
|
3865
3750
|
*
|
|
3866
3751
|
* Pushing is mine either way, and `commits` says whether committing is too.
|
|
@@ -3953,7 +3838,7 @@ const fix = Command.make("fix", {
|
|
|
3953
3838
|
const file = Option.getOrElse(yield* read, () => ({}));
|
|
3954
3839
|
const { number, repo } = yield* named(pr, Object.keys(file.repos ?? {}).toSorted());
|
|
3955
3840
|
const settings = settingsFor(file, repo);
|
|
3956
|
-
const run = yield* currentRun(repo, number
|
|
3841
|
+
const run = yield* currentRun(repo, number);
|
|
3957
3842
|
const found = yield* whatItFound(run);
|
|
3958
3843
|
yield* Console.log(header$1(run, found, settings.stamp.blocks_on));
|
|
3959
3844
|
if (found.findings.length === 0) return;
|
|
@@ -3995,105 +3880,64 @@ const fix = Command.make("fix", {
|
|
|
3995
3880
|
...userFacing,
|
|
3996
3881
|
"GitFailed",
|
|
3997
3882
|
"WorktreeHeld",
|
|
3998
|
-
"
|
|
3883
|
+
"AgentFailed"
|
|
3999
3884
|
], asUserError))).pipe(Command.withDescription("Pick findings from the current review run and open a fix session on them"));
|
|
4000
3885
|
//#endregion
|
|
4001
3886
|
//#region src/cli/init.ts
|
|
4002
|
-
const runnerFlag = Flag.Literals("runner", [...runners]).pipe(Flag.withDescription("Which runner is my bar, on this machine"), Flag.optional);
|
|
4003
|
-
const codexFlag = Flag.Boolean("codex").pipe(Flag.withDescription("Ask Codex for a second opinion beside the runner that is my bar"), Flag.optional);
|
|
4004
3887
|
const effortFlag$1 = Flag.Literals("effort", [
|
|
4005
3888
|
"low",
|
|
4006
3889
|
"medium",
|
|
4007
|
-
"high"
|
|
4008
|
-
|
|
3890
|
+
"high",
|
|
3891
|
+
"xhigh",
|
|
3892
|
+
"max"
|
|
3893
|
+
]).pipe(Flag.withDescription("How much a review run spends on this repository"), Flag.optional);
|
|
4009
3894
|
const baseFlag = Flag.String("base").pipe(Flag.withDescription("The branch this repository's pull requests target, over the default one"), Flag.optional);
|
|
4010
|
-
const askRunner = Prompt.Select({
|
|
4011
|
-
message: "Which runner is your bar?",
|
|
4012
|
-
choices: [
|
|
4013
|
-
{
|
|
4014
|
-
title: "builtin",
|
|
4015
|
-
value: "builtin",
|
|
4016
|
-
description: "Claude Code's own code review"
|
|
4017
|
-
},
|
|
4018
|
-
{
|
|
4019
|
-
title: "prompt",
|
|
4020
|
-
value: "prompt",
|
|
4021
|
-
description: "The tool's own review prompt, on Claude Code"
|
|
4022
|
-
},
|
|
4023
|
-
{
|
|
4024
|
-
title: "codex",
|
|
4025
|
-
value: "codex",
|
|
4026
|
-
description: "The tool's own review prompt, on the Codex CLI"
|
|
4027
|
-
}
|
|
4028
|
-
]
|
|
4029
|
-
});
|
|
4030
|
-
/**
|
|
4031
|
-
* The second question, and only where the first did not already answer it.
|
|
4032
|
-
*
|
|
4033
|
-
* Codex is the second opinion, so it is not one more thing in the list of what
|
|
4034
|
-
* my bar could be: it is a runner that runs beside it. A machine whose bar is
|
|
4035
|
-
* Codex has nothing left to ask.
|
|
4036
|
-
*/
|
|
4037
|
-
const askSecondOpinion = Prompt.Confirm({
|
|
4038
|
-
message: "Also ask Codex for a second opinion on every review run?",
|
|
4039
|
-
initial: false
|
|
4040
|
-
});
|
|
4041
|
-
/**
|
|
4042
|
-
* What the file should name: the runner that is my bar, and Codex beside it
|
|
4043
|
-
* where I asked for one.
|
|
4044
|
-
*
|
|
4045
|
-
* Codex alone is the review rather than a second opinion, which is the rule
|
|
4046
|
-
* `#domain/review.ts` draws too, so it is never listed twice.
|
|
4047
|
-
*/
|
|
4048
|
-
const listing = (bar, secondOpinion) => bar === "codex" || !secondOpinion ? [bar] : [bar, "codex"];
|
|
4049
|
-
const noRunnerChosen = "No runner chosen, so nothing was written. Pass --runner builtin, --runner prompt or --runner codex to choose without the prompt, and --codex for a second opinion beside it.";
|
|
4050
3895
|
/** The settings the flags asked for, and only those. */
|
|
4051
3896
|
const asked = (base, effort) => ({
|
|
4052
3897
|
...Option.isSome(base) ? { base: base.value } : {},
|
|
4053
3898
|
...Option.isSome(effort) ? { review: { effort: effort.value } } : {}
|
|
4054
3899
|
});
|
|
3900
|
+
/** What a review will open on, as the setup prints it back. */
|
|
3901
|
+
const opening = (defaults) => {
|
|
3902
|
+
const review = {
|
|
3903
|
+
...builtIn.review,
|
|
3904
|
+
...defaults.review
|
|
3905
|
+
};
|
|
3906
|
+
return review.command === null ? "my own prompt" : [review.command, review.effort].filter((part) => part !== null).join(" ");
|
|
3907
|
+
};
|
|
4055
3908
|
const row = (label, value) => `${label.padEnd(12)}${value}`;
|
|
4056
3909
|
/**
|
|
4057
3910
|
* Both the machine setup and the repository registration: there is deliberately
|
|
4058
3911
|
* no separate `setup` command.
|
|
4059
3912
|
*
|
|
4060
|
-
* The first run on a machine checks `gh
|
|
4061
|
-
*
|
|
4062
|
-
*
|
|
4063
|
-
*
|
|
4064
|
-
*
|
|
4065
|
-
* differently.
|
|
3913
|
+
* The first run on a machine checks `gh` and spells the defaults out in the
|
|
3914
|
+
* configuration file. Run inside a repository, it also registers that
|
|
3915
|
+
* `owner/repo`, taking the name from `gh` so I never type it. Run again, it
|
|
3916
|
+
* changes what the flags name, keeps every other setting the file already had,
|
|
3917
|
+
* and leaves the file untouched where nothing was decided differently.
|
|
4066
3918
|
*
|
|
4067
|
-
*
|
|
4068
|
-
*
|
|
4069
|
-
* the
|
|
3919
|
+
* It asks nothing. Reviews run on Claude Code, and what a run opens on is
|
|
3920
|
+
* `review.command` and `review.prompt` - a line and a paragraph that belong in
|
|
3921
|
+
* the file rather than in a terminal prompt.
|
|
3922
|
+
*
|
|
3923
|
+
* `--effort` and `--base` are about one repository, so they land on the
|
|
3924
|
+
* repository this ran in, or in the defaults when it ran outside one.
|
|
4070
3925
|
*/
|
|
4071
3926
|
const init = Command.make("init", {
|
|
4072
|
-
runner: runnerFlag,
|
|
4073
|
-
codex: codexFlag,
|
|
4074
3927
|
effort: effortFlag$1,
|
|
4075
3928
|
base: baseFlag
|
|
4076
|
-
}, Effect.fn("init")(function* ({ base,
|
|
3929
|
+
}, Effect.fn("init")(function* ({ base, effort }) {
|
|
4077
3930
|
yield* requireAuth;
|
|
4078
3931
|
const config = yield* ConfigStore;
|
|
4079
3932
|
const before = yield* read;
|
|
4080
3933
|
const file = Option.getOrElse(before, () => ({}));
|
|
4081
|
-
const
|
|
4082
|
-
const chosen = Option.isSome(runner) ? Option.some(runner.value) : firstRun ? Option.some(yield* askRunner) : Option.none();
|
|
4083
|
-
const listed = file.defaults?.review?.runners ?? builtIn.review.runners;
|
|
4084
|
-
const bar = Option.getOrElse(chosen, () => listed.find((it) => it !== "codex") ?? "builtin");
|
|
4085
|
-
const secondOpinion = firstRun && bar !== "codex" && Option.isNone(runner) && Option.isNone(codex) ? Option.some(yield* askSecondOpinion) : codex;
|
|
4086
|
-
const decided = Option.isSome(chosen) || Option.isSome(secondOpinion);
|
|
4087
|
-
const beside = Option.getOrElse(secondOpinion, () => listed.includes("codex"));
|
|
4088
|
-
const inherited = firstRun ? merge$1(builtIn, file.defaults ?? {}) : file.defaults ?? {};
|
|
4089
|
-
const defaults = merge$1(inherited, decided ? { review: { runners: listing(bar, beside) } } : {});
|
|
3934
|
+
const defaults = file.defaults === void 0 ? merge$1(builtIn, file.defaults ?? {}) : file.defaults ?? {};
|
|
4090
3935
|
const state = yield* stateDirectory;
|
|
4091
3936
|
const repo = yield* currentRepo.pipe(Effect.asSome, Effect.catchTag("NoRepository", () => Effect.succeedNone));
|
|
4092
3937
|
const overrides = asked(base, effort);
|
|
4093
3938
|
const written = Option.isSome(repo) ? withRepo(withDefaults(file, defaults), repo.value, overrides) : withDefaults(file, merge$1(defaults, overrides));
|
|
4094
3939
|
if (encode(written) !== encode(file) || Option.isNone(before)) yield* write(written);
|
|
4095
|
-
|
|
4096
|
-
yield* Console.log(row("runner", settled.join(", ")));
|
|
3940
|
+
yield* Console.log(row("review", opening(written.defaults ?? {})));
|
|
4097
3941
|
yield* Console.log(row("config", config.path));
|
|
4098
3942
|
yield* Console.log(row("state", state));
|
|
4099
3943
|
yield* Console.log(Option.isNone(repo) ? row("repository", "none here - run dw-mc init inside a repository to register it") : row("repository", `${repo.value} (${file.repos?.[repo.value] === void 0 ? "registered" : "already registered"})`));
|
|
@@ -4101,12 +3945,8 @@ const init = Command.make("init", {
|
|
|
4101
3945
|
"ConfigMalformed",
|
|
4102
3946
|
"GhUnauthenticated",
|
|
4103
3947
|
"GhUnavailable",
|
|
4104
|
-
"GhUnreadable"
|
|
4105
|
-
|
|
4106
|
-
], (cause) => Effect.fail(cause._tag === "QuitError" ? new CliError.UserError({
|
|
4107
|
-
cause,
|
|
4108
|
-
userMessage: noRunnerChosen
|
|
4109
|
-
}) : new CliError.UserError({ cause }))))).pipe(Command.withDescription("Set this machine up and register the repository I am in"));
|
|
3948
|
+
"GhUnreadable"
|
|
3949
|
+
], asUserError))).pipe(Command.withDescription("Set this machine up and register the repository I am in"));
|
|
4110
3950
|
//#endregion
|
|
4111
3951
|
//#region src/domain/stamp.ts
|
|
4112
3952
|
/**
|
|
@@ -4840,7 +4680,7 @@ const resolve = Command.make("resolve", {
|
|
|
4840
4680
|
...userFacing,
|
|
4841
4681
|
"GitFailed",
|
|
4842
4682
|
"WorktreeHeld",
|
|
4843
|
-
"
|
|
4683
|
+
"AgentFailed"
|
|
4844
4684
|
], asUserError))).pipe(Command.withDescription("Open a session on the conflict that stopped a rebase, in a worktree of my own"));
|
|
4845
4685
|
//#endregion
|
|
4846
4686
|
//#region src/adapters/notify.ts
|
|
@@ -4930,14 +4770,6 @@ const spinning = Effect.fnUntraced(function* (reads, use) {
|
|
|
4930
4770
|
//#endregion
|
|
4931
4771
|
//#region src/domain/persona.ts
|
|
4932
4772
|
/**
|
|
4933
|
-
* The review prompt the `prompt` runner owns.
|
|
4934
|
-
*
|
|
4935
|
-
* Where `builtin` drives the agent's own review command, this is a prompt the
|
|
4936
|
-
* tool carries, so my bar is not one agent's idea of a code review. It is the
|
|
4937
|
-
* same prompt on either CLI: what differs is how each one is handed the schema,
|
|
4938
|
-
* which belongs to the runner adapter and not here.
|
|
4939
|
-
*/
|
|
4940
|
-
/**
|
|
4941
4773
|
* The reviewer persona, derived from Addy Osmani's `code-reviewer` agent
|
|
4942
4774
|
* (`addyosmani/agent-skills`, MIT, see `NOTICE.md`).
|
|
4943
4775
|
*
|
|
@@ -4983,18 +4815,18 @@ the task or the pull request description before the code. Every Critical and Req
|
|
|
4983
4815
|
specific fix in its summary. Where you are uncertain, say so in the summary and say what would settle
|
|
4984
4816
|
it, rather than guessing.`;
|
|
4985
4817
|
/**
|
|
4986
|
-
* The prompt
|
|
4818
|
+
* The prompt a review run with no slash command opens on.
|
|
4987
4819
|
*
|
|
4988
4820
|
* It says what to review and how to answer, and nothing about how the answer is
|
|
4989
|
-
* validated: the schema arrives beside the prompt
|
|
4990
|
-
*
|
|
4821
|
+
* validated: the schema arrives beside the prompt, so describing it here would
|
|
4822
|
+
* be the same shape written twice.
|
|
4991
4823
|
*
|
|
4992
|
-
* `review.
|
|
4993
|
-
* spells it. A repository
|
|
4994
|
-
*
|
|
4824
|
+
* `review.prompt` is a passthrough and goes in first, spelled exactly as the
|
|
4825
|
+
* file spells it. A repository with its own instructions gets its review with
|
|
4826
|
+
* the persona behind it, and the tool does not try to interpret the value.
|
|
4995
4827
|
*/
|
|
4996
4828
|
const reviewPrompt = (reviewing) => [
|
|
4997
|
-
...reviewing.
|
|
4829
|
+
...reviewing.prompt === null ? [] : [reviewing.prompt, ""],
|
|
4998
4830
|
persona,
|
|
4999
4831
|
"",
|
|
5000
4832
|
`The change is ${reviewing.repo}#${reviewing.number}, "${reviewing.title}".`,
|
|
@@ -5005,103 +4837,104 @@ const reviewPrompt = (reviewing) => [
|
|
|
5005
4837
|
"line it is at, its severity and a one-sentence summary. The verdict is clean when there is",
|
|
5006
4838
|
"nothing to report, and findings otherwise."
|
|
5007
4839
|
].join("\n");
|
|
4840
|
+
/**
|
|
4841
|
+
* What one review run opens on, decided by what the repository configured.
|
|
4842
|
+
*
|
|
4843
|
+
* A slash command is the review, so the persona stays out of its way and my own
|
|
4844
|
+
* instructions ride beside it. Without one the review is the tool's own, and my
|
|
4845
|
+
* instructions go in front of the persona. The effort word follows the command
|
|
4846
|
+
* because that is where a slash command takes its arguments; a repository that
|
|
4847
|
+
* spells its own arguments out sets `review.effort` to null and keeps the line.
|
|
4848
|
+
*/
|
|
4849
|
+
const turnFor = (review, about) => review.command === null ? {
|
|
4850
|
+
_tag: "prompt",
|
|
4851
|
+
text: reviewPrompt({
|
|
4852
|
+
...about,
|
|
4853
|
+
prompt: review.prompt
|
|
4854
|
+
})
|
|
4855
|
+
} : {
|
|
4856
|
+
_tag: "command",
|
|
4857
|
+
line: [review.command, review.effort].filter((part) => part !== null).join(" "),
|
|
4858
|
+
instructions: review.prompt
|
|
4859
|
+
};
|
|
5008
4860
|
//#endregion
|
|
5009
4861
|
//#region src/cli/review.ts
|
|
5010
4862
|
/** What the spinner says a run has got through, while it is still going. */
|
|
5011
|
-
const saying = (
|
|
5012
|
-
|
|
4863
|
+
const saying = (doing, since) => [
|
|
4864
|
+
"reviewing",
|
|
5013
4865
|
count(doing.tools, "tool"),
|
|
5014
4866
|
doing.subagents === 0 ? null : count(doing.subagents, "subagent"),
|
|
5015
4867
|
since
|
|
5016
4868
|
].filter((part) => part !== null).join(" · ");
|
|
4869
|
+
const commandFlag = Flag.String("command").pipe(Flag.withDescription("The slash command this run opens on, over what the repository configured"), Flag.optional);
|
|
4870
|
+
const promptFlag = Flag.String("prompt").pipe(Flag.withDescription("The review instructions this run carries, over what the repository configured"), Flag.optional);
|
|
5017
4871
|
const effortFlag = Flag.Literals("effort", [
|
|
5018
4872
|
"low",
|
|
5019
4873
|
"medium",
|
|
5020
|
-
"high"
|
|
4874
|
+
"high",
|
|
4875
|
+
"xhigh",
|
|
4876
|
+
"max"
|
|
5021
4877
|
]).pipe(Flag.withDescription("How much this run spends, over what the repository configured"), Flag.optional);
|
|
4878
|
+
const modelFlag = Flag.String("model").pipe(Flag.withDescription("The model this run reads the code on, over what the repository configured"), Flag.optional);
|
|
4879
|
+
const promptOnlyFlag = Flag.Boolean("prompt-only").pipe(Flag.withDefault(false), Flag.withDescription("Review on the prompt alone, whatever slash command the repository configured"));
|
|
4880
|
+
const commandOnlyFlag = Flag.Boolean("command-only").pipe(Flag.withDefault(false), Flag.withDescription("Review on the slash command alone, whatever instructions the repository configured"));
|
|
5022
4881
|
const forceFlag$1 = Flag.Boolean("force").pipe(Flag.withDefault(false), Flag.withDescription("Review even where the re-run rule would skip it"));
|
|
5023
|
-
/**
|
|
5024
|
-
const
|
|
5025
|
-
|
|
5026
|
-
|
|
4882
|
+
/** A flag that names a value beside the flag that clears it: one of the two, never both. */
|
|
4883
|
+
const opposite = (flag, given, only) => Option.isSome(given) ? [`--${flag} and --${only} say opposite things. Pass one.`] : [];
|
|
4884
|
+
/** What this run is asked, once the flags have had their say over the file. */
|
|
4885
|
+
const asking = (options) => {
|
|
4886
|
+
const clash = [...options.promptOnly ? opposite("command", options.command, "prompt-only") : [], ...options.commandOnly ? opposite("prompt", options.prompt, "command-only") : []];
|
|
4887
|
+
if (clash.length > 0) return Effect.fail(new CliError.UserError({ cause: clash.join(" ") }));
|
|
4888
|
+
const { review } = options.settings;
|
|
4889
|
+
return Effect.succeed({
|
|
4890
|
+
command: options.promptOnly ? null : Option.getOrElse(options.command, () => review.command),
|
|
4891
|
+
effort: Option.getOrElse(options.effort, () => review.effort),
|
|
4892
|
+
prompt: options.commandOnly ? null : Option.getOrElse(options.prompt, () => review.prompt),
|
|
4893
|
+
model: Option.getOrElse(options.model, () => review.model)
|
|
4894
|
+
});
|
|
5027
4895
|
};
|
|
5028
4896
|
/**
|
|
5029
4897
|
* What the re-run rule is asked about, read before anything is cut or spawned:
|
|
5030
4898
|
* the whole point of the rule is not paying for the run.
|
|
5031
4899
|
*
|
|
5032
|
-
* It is asked per runner, because a second opinion that has never seen this
|
|
5033
|
-
* pull request is not skipped for the head the primary review already read.
|
|
5034
|
-
*
|
|
5035
4900
|
* GitHub is asked what changed only where there is a run to measure from and a
|
|
5036
4901
|
* different head to measure to. Neither is the rule deciding anything - there is
|
|
5037
4902
|
* simply nothing to compare - and a comparison GitHub would not answer comes
|
|
5038
4903
|
* back as nothing known rather than as a failure of the command.
|
|
5039
4904
|
*/
|
|
5040
|
-
const askedOf = Effect.fn("review.askedOf")(function* (repo, number, head
|
|
5041
|
-
const last = Option.getOrNull(yield* lastRun(repo, number
|
|
4905
|
+
const askedOf = Effect.fn("review.askedOf")(function* (repo, number, head) {
|
|
4906
|
+
const last = Option.getOrNull(yield* lastRun(repo, number));
|
|
5042
4907
|
return {
|
|
5043
4908
|
last,
|
|
5044
4909
|
head,
|
|
5045
4910
|
changed: last === null || last.head === head ? null : Option.getOrNull(yield* Effect.option(comparedFiles(repo, last.head, head)))
|
|
5046
4911
|
};
|
|
5047
4912
|
});
|
|
4913
|
+
/** How a run reads on the line above it: what it opens on, and on which model. */
|
|
4914
|
+
const spending = (turn, model) => [
|
|
4915
|
+
turn._tag === "command" ? turn.line : "the tool's own prompt",
|
|
4916
|
+
turn._tag === "command" && turn.instructions !== null ? "with my own instructions" : null,
|
|
4917
|
+
model === null ? null : `model ${model}`
|
|
4918
|
+
].filter((part) => part !== null).join(", ");
|
|
5048
4919
|
/**
|
|
5049
|
-
*
|
|
5050
|
-
* spend.
|
|
5051
|
-
*
|
|
5052
|
-
* `review.effort` drives the built-in review command and nothing else, and
|
|
5053
|
-
* `review.model` drives the prompt the tool owns, so each run says the one that
|
|
5054
|
-
* decided anything about it.
|
|
5055
|
-
*/
|
|
5056
|
-
const spending = (runner, effort, model) => {
|
|
5057
|
-
if (runner === "builtin") return `${runner}, effort ${effort}`;
|
|
5058
|
-
return model === null ? runner : `${runner}, model ${model}`;
|
|
5059
|
-
};
|
|
5060
|
-
/**
|
|
5061
|
-
* One runner's review of the head in the worktree.
|
|
5062
|
-
*
|
|
5063
|
-
* `builtin` is two turns of one session: the agent's own review as prose, then
|
|
5064
|
-
* the same review reported as findings. `prompt` and `codex` are one turn on
|
|
5065
|
-
* the tool's own prompt, which carries the schema with it, so the findings are
|
|
5066
|
-
* what the run answers with.
|
|
4920
|
+
* The review of the head in the worktree.
|
|
5067
4921
|
*
|
|
5068
4922
|
* Whatever the reporting comes to is a value and not a failure: the review is
|
|
5069
4923
|
* already worth keeping, and a turn that could not report is recorded as the
|
|
5070
4924
|
* failure it is rather than lost with it.
|
|
5071
4925
|
*/
|
|
5072
4926
|
const reviewOn = Effect.fn("review.reviewOn")(function* (options) {
|
|
5073
|
-
const
|
|
5074
|
-
const
|
|
5075
|
-
if (options.runner === "builtin") {
|
|
5076
|
-
const turn = yield* spinning(said, (onTool) => builtinReview({
|
|
5077
|
-
launcher,
|
|
5078
|
-
directory,
|
|
5079
|
-
effort: options.effort,
|
|
5080
|
-
onTool
|
|
5081
|
-
}));
|
|
5082
|
-
const reported = yield* Effect.result(Effect.flatMap(builtinFindings({
|
|
5083
|
-
launcher,
|
|
5084
|
-
directory,
|
|
5085
|
-
sessionId: turn.sessionId,
|
|
5086
|
-
jsonSchema
|
|
5087
|
-
}), (output) => Schema.decodeUnknownEffect(Reported)(output)));
|
|
5088
|
-
return {
|
|
5089
|
-
sessionId: turn.sessionId,
|
|
5090
|
-
prose: turn.report,
|
|
5091
|
-
reported
|
|
5092
|
-
};
|
|
5093
|
-
}
|
|
5094
|
-
const prompt = reviewPrompt(options.about);
|
|
5095
|
-
const run = yield* spinning(said, (onTool) => promptRun({
|
|
5096
|
-
runner: options.runner,
|
|
4927
|
+
const { directory, launcher, model, turn } = options;
|
|
4928
|
+
const run = yield* spinning(saying, (onTool) => reviewTurns({
|
|
5097
4929
|
launcher,
|
|
5098
4930
|
directory,
|
|
5099
|
-
|
|
5100
|
-
model
|
|
4931
|
+
turn,
|
|
4932
|
+
model,
|
|
5101
4933
|
jsonSchema,
|
|
5102
4934
|
onTool
|
|
5103
4935
|
}));
|
|
5104
|
-
const
|
|
4936
|
+
const answered = Result.isFailure(run.findings) ? Effect.fail(run.findings.failure) : Effect.succeed(run.findings.success);
|
|
4937
|
+
const reported = yield* Effect.result(Effect.flatMap(answered, (output) => Schema.decodeUnknownEffect(Reported)(output)));
|
|
5105
4938
|
return {
|
|
5106
4939
|
sessionId: run.sessionId,
|
|
5107
4940
|
prose: run.prose,
|
|
@@ -5109,12 +4942,12 @@ const reviewOn = Effect.fn("review.reviewOn")(function* (options) {
|
|
|
5109
4942
|
};
|
|
5110
4943
|
});
|
|
5111
4944
|
/**
|
|
5112
|
-
* What
|
|
5113
|
-
*
|
|
4945
|
+
* What the review comes to on disk: the findings it reported, or the failure it
|
|
4946
|
+
* reached instead.
|
|
5114
4947
|
*
|
|
5115
|
-
* A
|
|
5116
|
-
*
|
|
5117
|
-
*
|
|
4948
|
+
* A run that would not start is as much a failure as a turn that answered in a
|
|
4949
|
+
* shape that does not validate, and both are recorded: the head has been tried
|
|
4950
|
+
* and nothing was found, which is not the same as nothing being wrong.
|
|
5118
4951
|
*/
|
|
5119
4952
|
const ranBy = (got) => {
|
|
5120
4953
|
if (Result.isFailure(got)) return {
|
|
@@ -5146,20 +4979,14 @@ const ranBy = (got) => {
|
|
|
5146
4979
|
};
|
|
5147
4980
|
};
|
|
5148
4981
|
/**
|
|
5149
|
-
* The command's own failure where
|
|
5150
|
-
* nothing, and nothing where they all reported.
|
|
4982
|
+
* The command's own failure where the review reported nothing.
|
|
5151
4983
|
*
|
|
5152
|
-
*
|
|
5153
|
-
* review I asked for is one to trust.
|
|
5154
|
-
* said out loud and no more: it informs me, so its silence is not my command
|
|
5155
|
-
* failing.
|
|
4984
|
+
* The run is written down either way; what the exit code says is whether the
|
|
4985
|
+
* review I asked for is one to trust.
|
|
5156
4986
|
*/
|
|
5157
|
-
const unreported = (
|
|
5158
|
-
const
|
|
5159
|
-
|
|
5160
|
-
return deciding.includes(run.runner) && detail !== null ? [`${run.runner}: ${detail}`] : [];
|
|
5161
|
-
});
|
|
5162
|
-
return failed.length === 0 ? Effect.void : Effect.fail(new CliError.UserError({ cause: `The review ran and its findings did not: ${failed.join("; ")}. Run dw-mc review ${number} --force to run it again.` }));
|
|
4987
|
+
const unreported = (run, number) => {
|
|
4988
|
+
const detail = detailOf(run);
|
|
4989
|
+
return detail === null ? Effect.void : Effect.fail(new CliError.UserError({ cause: `The review ran and its findings did not: ${detail}. Run dw-mc review ${number} --force to run it again.` }));
|
|
5163
4990
|
};
|
|
5164
4991
|
/**
|
|
5165
4992
|
* One review run, started by hand, in the foreground.
|
|
@@ -5169,109 +4996,102 @@ const unreported = (recorded, deciding, number) => {
|
|
|
5169
4996
|
* the one who decides to spend it.
|
|
5170
4997
|
*
|
|
5171
4998
|
* The run happens in a throwaway worktree of the tool's own clone, so what is
|
|
5172
|
-
* reviewed is the pull request's head rather than whatever I have open.
|
|
5173
|
-
* configured runner reviews in that one worktree, one after the other: the
|
|
5174
|
-
* cutting is the expensive part that they can share, and a second opinion is
|
|
5175
|
-
* worth nothing if it read different code.
|
|
4999
|
+
* reviewed is the pull request's head rather than whatever I have open.
|
|
5176
5000
|
*
|
|
5177
|
-
* What
|
|
5178
|
-
*
|
|
5179
|
-
* finding later puts into Needs me.
|
|
5001
|
+
* What it found is kept against that head, which is what takes the pull request
|
|
5002
|
+
* out of Needs review run and what a blocking finding later puts into Needs me.
|
|
5180
5003
|
*
|
|
5181
5004
|
* The report is printed as well as kept. A run I waited minutes for should not
|
|
5182
5005
|
* need a second command to read.
|
|
5183
5006
|
*/
|
|
5184
5007
|
const review = Command.make("review", {
|
|
5185
5008
|
pr: prArgument,
|
|
5009
|
+
command: commandFlag,
|
|
5010
|
+
prompt: promptFlag,
|
|
5186
5011
|
effort: effortFlag,
|
|
5012
|
+
model: modelFlag,
|
|
5013
|
+
promptOnly: promptOnlyFlag,
|
|
5014
|
+
commandOnly: commandOnlyFlag,
|
|
5187
5015
|
force: forceFlag$1
|
|
5188
|
-
}, Effect.fn("review")(function* ({ effort, force, pr }) {
|
|
5016
|
+
}, Effect.fn("review")(function* ({ command, commandOnly, effort, force, model, pr, prompt, promptOnly }) {
|
|
5189
5017
|
const file = Option.getOrElse(yield* read, () => ({}));
|
|
5190
5018
|
const { number, repo } = yield* named(pr, Object.keys(file.repos ?? {}).toSorted());
|
|
5191
5019
|
const settings = settingsFor(file, repo);
|
|
5192
5020
|
const launcher = launcherOf(file);
|
|
5193
|
-
const
|
|
5194
|
-
|
|
5195
|
-
|
|
5021
|
+
const asked = yield* asking({
|
|
5022
|
+
settings,
|
|
5023
|
+
command,
|
|
5024
|
+
prompt,
|
|
5025
|
+
effort,
|
|
5026
|
+
model,
|
|
5027
|
+
promptOnly,
|
|
5028
|
+
commandOnly
|
|
5029
|
+
});
|
|
5196
5030
|
const view = yield* prView(repo, number);
|
|
5197
5031
|
yield* Console.log(`${repo}#${number} ${view.title}`);
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
since
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
const running = asked.filter((it) => it.since === null).map((it) => it.runner);
|
|
5204
|
-
if (running.length === 0) return;
|
|
5032
|
+
const since = force ? null : skippedSince(yield* askedOf(repo, number, view.headRefOid), settings.review.docs_only);
|
|
5033
|
+
if (since !== null) {
|
|
5034
|
+
yield* Console.log(` only documentation changed since ${short(since)}, so this run is skipped. Pass --force to review it anyway.`);
|
|
5035
|
+
return;
|
|
5036
|
+
}
|
|
5205
5037
|
const about = {
|
|
5206
5038
|
repo,
|
|
5207
5039
|
number,
|
|
5208
5040
|
title: view.title,
|
|
5209
5041
|
base: view.baseRefName,
|
|
5210
|
-
|
|
5042
|
+
prompt: asked.prompt
|
|
5211
5043
|
};
|
|
5044
|
+
const turn = turnFor(asked, about);
|
|
5212
5045
|
yield* Effect.gen(function* () {
|
|
5213
5046
|
const ran = yield* withWorktree(repo, number, (worktree) => Effect.gen(function* () {
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
effort: spend,
|
|
5222
|
-
settings,
|
|
5223
|
-
about
|
|
5224
|
-
}));
|
|
5225
|
-
done.push({
|
|
5226
|
-
runner,
|
|
5227
|
-
ran: ranBy(got)
|
|
5228
|
-
});
|
|
5229
|
-
}
|
|
5047
|
+
yield* Console.log(` head ${short(worktree.head)} ${spending(turn, asked.model)}`);
|
|
5048
|
+
const got = yield* Effect.result(reviewOn({
|
|
5049
|
+
launcher,
|
|
5050
|
+
directory: worktree.directory,
|
|
5051
|
+
turn,
|
|
5052
|
+
model: asked.model
|
|
5053
|
+
}));
|
|
5230
5054
|
return {
|
|
5231
5055
|
head: worktree.head,
|
|
5232
|
-
|
|
5056
|
+
ran: ranBy(got)
|
|
5233
5057
|
};
|
|
5234
5058
|
}));
|
|
5235
5059
|
const ranAt = yield* DateTime.now;
|
|
5236
5060
|
const runs = yield* storeFor("runs", ReviewRun);
|
|
5237
5061
|
const latest = yield* storeFor("runs", LastReviewed);
|
|
5238
5062
|
const reports = yield* textStoreFor("runs");
|
|
5239
|
-
const
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
const detail = detailOf(run);
|
|
5258
|
-
if (detail !== null) {
|
|
5259
|
-
yield* Console.log(` reported nothing: ${detail}`);
|
|
5260
|
-
continue;
|
|
5261
|
-
}
|
|
5063
|
+
const got = ran.ran;
|
|
5064
|
+
const run = {
|
|
5065
|
+
repo,
|
|
5066
|
+
number,
|
|
5067
|
+
head: ran.head,
|
|
5068
|
+
command: asked.command,
|
|
5069
|
+
effort: asked.command === null ? null : asked.effort,
|
|
5070
|
+
sessionId: got.sessionId,
|
|
5071
|
+
ranAt,
|
|
5072
|
+
outcome: got.outcome
|
|
5073
|
+
};
|
|
5074
|
+
yield* runs.set(runKey(repo, number, run.head), run);
|
|
5075
|
+
yield* latest.set(latestKey(repo, number), { head: run.head });
|
|
5076
|
+
yield* reports.set(reportKey(repo, number, run.head), reportDocument(run, view.title, got.prose ?? ""));
|
|
5077
|
+
yield* Console.log("");
|
|
5078
|
+
const detail = detailOf(run);
|
|
5079
|
+
if (detail !== null) yield* Console.log(` reported nothing: ${detail}`);
|
|
5080
|
+
else {
|
|
5262
5081
|
const found = reportedBy(run);
|
|
5263
|
-
if (found
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5082
|
+
if (found !== null) {
|
|
5083
|
+
if (got.prose !== null) {
|
|
5084
|
+
yield* Console.log(got.prose);
|
|
5085
|
+
yield* Console.log("");
|
|
5086
|
+
}
|
|
5087
|
+
yield* Console.log(summary(found, settings.stamp.blocks_on));
|
|
5088
|
+
for (const line of lines$1(found)) yield* Console.log(` ${line}`);
|
|
5267
5089
|
}
|
|
5268
|
-
yield* Console.log(summary(found, settings.stamp.blocks_on));
|
|
5269
|
-
for (const line of lines$1(found)) yield* Console.log(` ${line}`);
|
|
5270
5090
|
}
|
|
5271
5091
|
yield* Console.log(`Recorded against ${short(ran.head)} in ${yield* stateDirectory}`);
|
|
5272
|
-
yield* unreported(
|
|
5092
|
+
yield* unreported(run, number);
|
|
5273
5093
|
}).pipe(Effect.onExit((exit) => announce("dw-mc review", `${repo}#${number} ${Exit.isSuccess(exit) ? "reviewed" : "could not be reviewed"}`)));
|
|
5274
|
-
}, Effect.catchTag([...userFacing, "GitFailed"], asUserError))).pipe(Command.withDescription("Review one pull request on
|
|
5094
|
+
}, Effect.catchTag([...userFacing, "GitFailed"], asUserError))).pipe(Command.withDescription("Review one pull request on Claude Code, in a throwaway worktree"));
|
|
5275
5095
|
//#endregion
|
|
5276
5096
|
//#region src/cli/stamp.ts
|
|
5277
5097
|
const withdrawFlag = Flag.Boolean("withdraw").pipe(Flag.withDefault(false), Flag.withDescription("Take the stamp off this pull request, until its head changes"));
|
|
@@ -5429,7 +5249,7 @@ const uninstall = Command.make("uninstall", {
|
|
|
5429
5249
|
* Running from source leaves the constant undeclared rather than undefined, so
|
|
5430
5250
|
* the check has to be `typeof` and the fallback is what a test reads.
|
|
5431
5251
|
*/
|
|
5432
|
-
const version = "0.
|
|
5252
|
+
const version = "0.4.0";
|
|
5433
5253
|
/** Where the project lives, printed beside the version in the header. */
|
|
5434
5254
|
const projectUrl = "github.com/dominikwozniak/dw-mc";
|
|
5435
5255
|
const subcommands = [
|