@jphutchins/code-review 0.1.0-alpha.12 → 0.1.0-alpha.13

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/index.js CHANGED
@@ -178,6 +178,16 @@ ${marker}` : "";
178
178
  };
179
179
  var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
180
180
  var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
181
+ var parseFindingsMarker = (body) => {
182
+ const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
183
+ const b64 = match?.[1];
184
+ if (b64 === void 0) return null;
185
+ try {
186
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
187
+ } catch {
188
+ return null;
189
+ }
190
+ };
181
191
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
182
192
  var projectPatch = (patch) => {
183
193
  if (patch === void 0) return { kind: "none" };
@@ -234,6 +244,7 @@ var render = (input) => {
234
244
  postedAt: input.postedAt ?? "",
235
245
  severityCounts: input.severityCounts ?? computeSeverityCounts(input.findings.findings),
236
246
  strays: (input.strays ?? []).map(sanitizeFinding),
247
+ unanchoredCount: input.unanchoredCount ?? 0,
237
248
  inlineDisposition: input.inlineDisposition ?? null,
238
249
  runUrl: input.runUrl ?? null,
239
250
  jsonUrl: input.jsonUrl ?? null,
@@ -355,7 +366,7 @@ var buildInlineComments = (findings, diff, context) => {
355
366
  }
356
367
  return comment;
357
368
  });
358
- return { comments, strays };
369
+ return { comments, strays, inDiff };
359
370
  };
360
371
  var renderStraysSection = (strays) => {
361
372
  if (strays.length === 0) return "";
@@ -1089,25 +1100,50 @@ var parseHtmlUrl = (raw) => {
1089
1100
  return void 0;
1090
1101
  }
1091
1102
  };
1092
- var postInlineReview = async (repo, prNumber, headSha, comments, stickyUrl, marker, ghApi) => {
1103
+ var commentPayload = (c) => ({
1104
+ path: c.path,
1105
+ line: c.line,
1106
+ side: c.side,
1107
+ ...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
1108
+ body: formatMarkdown(c.body)
1109
+ });
1110
+ var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, marker, ghApi) => {
1093
1111
  const pointer = reviewBodyPointer(headSha, stickyUrl, marker);
1094
- const body = JSON.stringify({
1112
+ const reviewBody = (withComments) => JSON.stringify({
1095
1113
  body: pointer,
1096
1114
  commit_id: headSha,
1097
1115
  event: "COMMENT",
1098
- comments: comments.map((c) => ({
1099
- path: c.path,
1100
- line: c.line,
1101
- side: c.side,
1102
- ...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
1103
- body: formatMarkdown(c.body)
1104
- }))
1116
+ comments: withComments ? comments.map(commentPayload) : []
1105
1117
  });
1106
- const stdout = await ghApi(
1107
- [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"],
1108
- body
1109
- );
1110
- return parseHtmlUrl(stdout);
1118
+ const reviewsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"];
1119
+ try {
1120
+ const stdout = await ghApi(reviewsEndpoint, reviewBody(true));
1121
+ return { url: parseHtmlUrl(stdout), inlinePosted: comments.length, unposted: [] };
1122
+ } catch (err) {
1123
+ if (comments.length === 0) throw err;
1124
+ process.stderr.write(
1125
+ `Warning: the batched inline review on PR #${String(prNumber)} was rejected (${err instanceof Error ? err.message : String(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
1126
+ `
1127
+ );
1128
+ const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
1129
+ const commentsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/comments`, "--input", "-"];
1130
+ const unposted = [];
1131
+ let inlinePosted = 0;
1132
+ for (const [i, c] of comments.entries()) {
1133
+ try {
1134
+ await ghApi(commentsEndpoint, JSON.stringify({ commit_id: headSha, ...commentPayload(c) }));
1135
+ inlinePosted += 1;
1136
+ } catch (e) {
1137
+ const finding = inDiff[i];
1138
+ if (finding) unposted.push(finding);
1139
+ process.stderr.write(
1140
+ `Warning: inline comment on ${c.path}:${String(c.line)} rejected (${e instanceof Error ? e.message : String(e)}) \u2014 surfacing that finding in the sticky instead (issue #57)
1141
+ `
1142
+ );
1143
+ }
1144
+ }
1145
+ return { url, inlinePosted, unposted };
1146
+ }
1111
1147
  };
1112
1148
  var findBotComment = async (repo, prNumber, botLogin, marker, ghApi) => {
1113
1149
  const stdout = await ghApi(
@@ -1174,10 +1210,7 @@ var fetchBotReviews = async (repo, prNumber, botLogin, ghApi) => {
1174
1210
  return [];
1175
1211
  }
1176
1212
  if (!Array.isArray(reviews)) return [];
1177
- return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({
1178
- id: r.id,
1179
- commitId: typeof r.commit_id === "string" ? r.commit_id : ""
1180
- }));
1213
+ return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({ id: r.id }));
1181
1214
  };
1182
1215
  var dismissReviews = async (repo, prNumber, ids, ghApi) => {
1183
1216
  for (const id of ids) {
@@ -1200,16 +1233,15 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
1200
1233
  }
1201
1234
  }
1202
1235
  };
1203
- var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login} originalCommit{oid}}}}}}}}";
1236
+ var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login}}}}}}}}";
1204
1237
  var MINIMIZE_COMMENT_MUTATION = "mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:OUTDATED}){minimizedComment{isMinimized}}}";
1205
- var supersededCommentId = (c, headSha, logins) => {
1238
+ var priorBotCommentId = (c, logins) => {
1206
1239
  if (typeof c !== "object" || c === null) return null;
1207
1240
  const o = c;
1208
1241
  const login = o.author?.login;
1209
- const oid = o.originalCommit?.oid;
1210
- return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) && typeof oid === "string" && oid !== headSha ? o.id : null;
1242
+ return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) ? o.id : null;
1211
1243
  };
1212
- var supersededBotCommentIds = (raw, headSha, botLogin) => {
1244
+ var priorBotCommentIds = (raw, botLogin) => {
1213
1245
  let parsed;
1214
1246
  try {
1215
1247
  parsed = JSON.parse(raw);
@@ -1223,13 +1255,13 @@ var supersededBotCommentIds = (raw, headSha, botLogin) => {
1223
1255
  const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
1224
1256
  const ids = nodes.flatMap((t4) => {
1225
1257
  const cnodes = t4.comments?.nodes;
1226
- return Array.isArray(cnodes) ? cnodes.map((c) => supersededCommentId(c, headSha, logins)).filter((id) => id !== null) : [];
1258
+ return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
1227
1259
  });
1228
1260
  return { ids, truncated };
1229
1261
  };
1230
- var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi) => {
1262
+ var listPriorBotCommentIds = async (repo, prNumber, botLogin, ghApi) => {
1231
1263
  const slash = repo.indexOf("/");
1232
- if (slash <= 0) return;
1264
+ if (slash <= 0) return [];
1233
1265
  const owner = repo.slice(0, slash);
1234
1266
  const name = repo.slice(slash + 1);
1235
1267
  let raw;
@@ -1250,15 +1282,18 @@ var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi
1250
1282
  `Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
1251
1283
  `
1252
1284
  );
1253
- return;
1285
+ return [];
1254
1286
  }
1255
- const { ids, truncated } = supersededBotCommentIds(raw, headSha, botLogin);
1287
+ const { ids, truncated } = priorBotCommentIds(raw, botLogin);
1256
1288
  if (truncated) {
1257
1289
  process.stderr.write(
1258
1290
  `Note: PR #${String(prNumber)} has more than 100 review threads \u2014 only the first 100 were scanned for stale bot comments
1259
1291
  `
1260
1292
  );
1261
1293
  }
1294
+ return ids;
1295
+ };
1296
+ var minimizeComments = async (prNumber, ids, ghApi) => {
1262
1297
  let minimized = 0;
1263
1298
  for (const id of ids) {
1264
1299
  try {
@@ -1373,7 +1408,11 @@ var post = async (input, ghApi = runGhApi) => {
1373
1408
  process.exit(0);
1374
1409
  }
1375
1410
  const findingsMarker = findingsPointer(findings, input.jsonUrl);
1376
- const { comments: rawComments, strays } = buildInlineComments(findings.findings, diff, {
1411
+ const {
1412
+ comments: rawComments,
1413
+ strays,
1414
+ inDiff
1415
+ } = buildInlineComments(findings.findings, diff, {
1377
1416
  inlineTemplate,
1378
1417
  models: envelope.models.map((m) => m.model),
1379
1418
  findings,
@@ -1387,8 +1426,7 @@ var post = async (input, ghApi = runGhApi) => {
1387
1426
  );
1388
1427
  }
1389
1428
  const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
1390
- const alreadyReviewedThisSha = botReviews.some((r) => r.commitId === input.headSha);
1391
- const initialDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : void 0 : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
1429
+ const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
1392
1430
  const commonRenderInput = {
1393
1431
  findings,
1394
1432
  envelope,
@@ -1412,7 +1450,15 @@ var post = async (input, ghApi = runGhApi) => {
1412
1450
 
1413
1451
  > **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
1414
1452
  ` : "";
1415
- const renderBody = (inlineDisposition, reviewUrl2) => formatMarkdown(render({ ...commonRenderInput, inlineDisposition, reviewUrl: reviewUrl2 }) + longFilesNote);
1453
+ const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
1454
+ render({
1455
+ ...commonRenderInput,
1456
+ ...straysOverride ? { strays: straysOverride } : {},
1457
+ ...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
1458
+ inlineDisposition,
1459
+ reviewUrl: reviewUrl2
1460
+ }) + longFilesNote
1461
+ );
1416
1462
  const stickyRef = await upsertSticky(
1417
1463
  input.repo,
1418
1464
  prNumber,
@@ -1420,49 +1466,53 @@ var post = async (input, ghApi = runGhApi) => {
1420
1466
  renderBody(initialDisposition),
1421
1467
  ghApi
1422
1468
  );
1423
- if (alreadyReviewedThisSha) {
1424
- process.stderr.write(
1425
- `A completed bot review already exists for ${input.headSha} \u2014 updated sticky only, no new review
1426
- `
1427
- );
1428
- return;
1429
- }
1430
- const stalePriorReviewIds = botReviews.filter((r) => r.commitId !== input.headSha).map((r) => r.id);
1431
- if (stalePriorReviewIds.length > 0) {
1432
- await dismissReviews(input.repo, prNumber, stalePriorReviewIds, ghApi);
1433
- }
1434
- const reviewUrl = await postInlineReview(
1469
+ const priorInlineComments = await listPriorBotCommentIds(
1470
+ input.repo,
1471
+ prNumber,
1472
+ input.botLogin,
1473
+ ghApi
1474
+ );
1475
+ const {
1476
+ url: reviewUrl,
1477
+ inlinePosted,
1478
+ unposted
1479
+ } = await postInlineReview(
1435
1480
  input.repo,
1436
1481
  prNumber,
1437
1482
  input.headSha,
1438
1483
  comments,
1484
+ inDiff,
1439
1485
  stickyRef?.url,
1440
1486
  findingsMarker,
1441
1487
  ghApi
1442
1488
  );
1443
1489
  process.stderr.write(
1444
- `Posted a review with ${String(comments.length)} inline comment(s) on PR #${String(prNumber)}
1490
+ `Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
1445
1491
  `
1446
1492
  );
1447
- await minimizeSupersededComments(input.repo, prNumber, input.headSha, input.botLogin, ghApi);
1448
- if (comments.length > 0 && stickyRef !== null) {
1449
- const confirmedDisposition = {
1450
- kind: "posted",
1451
- count: comments.length,
1452
- sha: input.headSha
1453
- };
1493
+ const priorReviewIds = botReviews.map((r) => r.id);
1494
+ if (priorReviewIds.length > 0) {
1495
+ await dismissReviews(input.repo, prNumber, priorReviewIds, ghApi);
1496
+ }
1497
+ await minimizeComments(prNumber, priorInlineComments, ghApi);
1498
+ const unanchoredCount = unposted.length;
1499
+ const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
1500
+ if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
1501
+ const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
1454
1502
  try {
1455
1503
  await patchComment(
1456
1504
  input.repo,
1457
1505
  stickyRef.id,
1458
- renderBody(confirmedDisposition, reviewUrl),
1506
+ renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
1459
1507
  ghApi
1460
1508
  );
1461
- process.stderr.write(`Linked sticky comment #${String(stickyRef.id)} to the review
1462
- `);
1509
+ process.stderr.write(
1510
+ `Updated sticky comment #${String(stickyRef.id)} to reflect the review
1511
+ `
1512
+ );
1463
1513
  } catch (err) {
1464
1514
  process.stderr.write(
1465
- `Warning: failed to link the sticky summary to the review: ${err instanceof Error ? err.message : String(err)}
1515
+ `Warning: failed to update the sticky summary after the review: ${err instanceof Error ? err.message : String(err)}
1466
1516
  `
1467
1517
  );
1468
1518
  }
@@ -1800,18 +1850,18 @@ var withMeta = (base, meta) => ({
1800
1850
  ...meta.effort ? { effort: meta.effort } : {}
1801
1851
  });
1802
1852
  var resolveTelemetry = (native, meta) => {
1803
- const fb = native.models.length === 0 ? meta.transcriptFallback?.() : void 0;
1804
- const useFallback = fb !== void 0 && fb.models.length > 0;
1853
+ const fb = (() => {
1854
+ try {
1855
+ return meta.transcriptFallback?.();
1856
+ } catch {
1857
+ return void 0;
1858
+ }
1859
+ })();
1860
+ const wallTurns = fb !== void 0 && fb.durationMs > 0 ? { turns: fb.turns, duration_ms: fb.durationMs } : { turns: native.turns, duration_ms: native.durationMs };
1805
1861
  return withMeta(
1806
- useFallback ? {
1807
- models: [...fb.models],
1808
- turns: fb.turns,
1809
- duration_ms: fb.durationMs,
1810
- vendor_cost_usd: native.vendorCostUsd
1811
- } : {
1812
- models: native.models,
1813
- turns: native.turns,
1814
- duration_ms: native.durationMs,
1862
+ {
1863
+ models: native.models.length > 0 ? native.models : fb ? [...fb.models] : native.models,
1864
+ ...wallTurns,
1815
1865
  vendor_cost_usd: native.vendorCostUsd
1816
1866
  },
1817
1867
  meta
@@ -2400,6 +2450,105 @@ ${printableSchema(schemaPath)}
2400
2450
  }
2401
2451
  }
2402
2452
  });
2453
+ var seedDraftCmd = defineCommand({
2454
+ meta: {
2455
+ name: "seed-draft",
2456
+ description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0 (issues #52, #53). Prints the mode chosen (prior|empty|none \u2014 none when even the scaffold write failed) to stdout; always exits 0"
2457
+ },
2458
+ args: {
2459
+ prior: {
2460
+ type: "string",
2461
+ description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and becomes the seed when it validates against the schema"
2462
+ },
2463
+ out: {
2464
+ type: "string",
2465
+ description: "Path to write the seed $DRAFT to (an absolute path outside the worktree)",
2466
+ required: true
2467
+ },
2468
+ kind: {
2469
+ type: "string",
2470
+ description: "Schema kind to validate the prior findings against (default: findings)"
2471
+ },
2472
+ schema: {
2473
+ type: "string",
2474
+ description: "Path to a schema file (wins over --kind/--schema-version)"
2475
+ },
2476
+ "schema-version": {
2477
+ type: "string",
2478
+ description: "Schema major.minor to validate the prior findings against (default: the kind's latest \u2014 an older-shaped prior review then falls back to the empty scaffold)"
2479
+ }
2480
+ },
2481
+ run: async ({ args }) => {
2482
+ const outPath = resolve$1(args.out);
2483
+ const kindArg = args.kind || "findings";
2484
+ const kind = isSchemaKind(kindArg) ? kindArg : "findings";
2485
+ if (kind !== kindArg) {
2486
+ process.stderr.write(
2487
+ `Warning: unknown --kind "${kindArg}" \u2014 validating against "findings"
2488
+ `
2489
+ );
2490
+ }
2491
+ const writeEmptyScaffold = () => {
2492
+ try {
2493
+ writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
2494
+ `);
2495
+ process.stderr.write(
2496
+ `Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
2497
+ `
2498
+ );
2499
+ process.stdout.write("empty\n");
2500
+ } catch (err) {
2501
+ process.stderr.write(
2502
+ `Warning: could not write the seed scaffold to ${outPath} (${err instanceof Error ? err.message : String(err)}) \u2014 the agent will create $DRAFT itself
2503
+ `
2504
+ );
2505
+ process.stdout.write("none\n");
2506
+ }
2507
+ };
2508
+ const priorFindings = (() => {
2509
+ if (!args.prior) return null;
2510
+ const raw = (() => {
2511
+ try {
2512
+ return JSON.parse(readFileSync(resolve$1(args.prior), "utf-8"));
2513
+ } catch {
2514
+ return null;
2515
+ }
2516
+ })();
2517
+ const body = typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
2518
+ return body === null ? null : parseFindingsMarker(body);
2519
+ })();
2520
+ if (priorFindings === null) {
2521
+ writeEmptyScaffold();
2522
+ return;
2523
+ }
2524
+ const seededFromPrior = (() => {
2525
+ try {
2526
+ const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
2527
+ if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
2528
+ writeFileSync(outPath, `${JSON.stringify(priorFindings, null, 2)}
2529
+ `);
2530
+ return true;
2531
+ } catch (err) {
2532
+ process.stderr.write(
2533
+ `Warning: could not seed from the prior review (${err instanceof Error ? err.message : String(err)}) \u2014 falling back to the empty scaffold
2534
+ `
2535
+ );
2536
+ return false;
2537
+ }
2538
+ })();
2539
+ if (seededFromPrior) {
2540
+ const priorList = priorFindings.findings;
2541
+ const count = Array.isArray(priorList) ? priorList.length : 0;
2542
+ process.stderr.write(
2543
+ `Seeded ${outPath} from the prior review (${String(count)} finding(s)) \u2014 verify each still holds against the current diff and refine in place
2544
+ `
2545
+ );
2546
+ process.stdout.write("prior\n");
2547
+ } else {
2548
+ writeEmptyScaffold();
2549
+ }
2550
+ }
2551
+ });
2403
2552
  var adaptCmd = defineCommand({
2404
2553
  meta: {
2405
2554
  name: "adapt",
@@ -2430,7 +2579,7 @@ var adaptCmd = defineCommand({
2430
2579
  },
2431
2580
  transcript: {
2432
2581
  type: "string",
2433
- description: "Path to the session transcript (the main .jsonl); when the native envelope carries no per-model usage \u2014 a wall-clock kill leaves it empty (issue #39) \u2014 telemetry is recovered from the transcript tree (main + subagents) so cost is real, not $0.00 (issue #36)"
2582
+ description: "Path to the session transcript (the main .jsonl). Its tree (main + subagents) is the source of the true wall + turn count \u2014 the native envelope only sees the main agent and under-reports a fan-out (issue #59) \u2014 and refills per-model usage too when the native has none (a wall-clock kill leaves it empty, so cost is real not $0.00 \u2014 issues #39/#36)"
2434
2583
  }
2435
2584
  },
2436
2585
  run: async ({ args }) => {
@@ -2844,7 +2993,7 @@ var main = defineCommand({
2844
2993
  meta: {
2845
2994
  name: "code-review",
2846
2995
  version: packageVersion,
2847
- description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, stop-gate, budget-hook, print-settings, and deadline"
2996
+ description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, seed-draft, stop-gate, budget-hook, print-settings, and deadline"
2848
2997
  },
2849
2998
  subCommands: {
2850
2999
  gather: gatherCmd,
@@ -2854,6 +3003,7 @@ var main = defineCommand({
2854
3003
  cost: costCmd,
2855
3004
  "check-cost": checkCostCmd,
2856
3005
  validate: validateCmd,
3006
+ "seed-draft": seedDraftCmd,
2857
3007
  adapt: adaptCmd,
2858
3008
  extract: extractCmd,
2859
3009
  "validate-patches": validatePatchesCmd,