@attalabs/vinaya 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +2 -0
  2. package/aeg-root/contracts/architect-planner.md +72 -0
  3. package/aeg-root/roles/architect.md +91 -0
  4. package/aeg-root/roles/planner.md +14 -15
  5. package/aeg-root/tranche-model.md +9 -1
  6. package/dist/checks/bin/check-body-bare-digits.js +60 -13
  7. package/dist/checks/bin/check-branch-topology.js +57 -12
  8. package/dist/checks/bin/check-brief-shape.js +57 -12
  9. package/dist/checks/bin/check-closes-n.js +57 -12
  10. package/dist/checks/bin/check-coherence.js +60 -13
  11. package/dist/checks/bin/check-dead-branch-push.js +57 -12
  12. package/dist/checks/bin/check-dispatch-readiness.js +127 -40
  13. package/dist/checks/bin/check-doc-coverage-push.js +60 -13
  14. package/dist/checks/bin/check-doc-coverage.js +60 -13
  15. package/dist/checks/bin/check-evidence-fresh.js +57 -12
  16. package/dist/checks/bin/check-first-push-dispatch.js +128 -41
  17. package/dist/checks/bin/check-issue-assignment.js +57 -12
  18. package/dist/checks/bin/check-no-disk-state.js +57 -12
  19. package/dist/checks/bin/check-reader-resolvable-prose.js +57 -12
  20. package/dist/checks/bin/check-registry-gates.js +57 -12
  21. package/dist/checks/bin/check-review-gate.js +60 -13
  22. package/dist/checks/bin/check-single-plan-pr.js +57 -12
  23. package/dist/checks/bin/check-test-plan.js +57 -12
  24. package/dist/index.js +663 -37
  25. package/package.json +1 -1
  26. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  27. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  28. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  29. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  30. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  31. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  32. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  33. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  35. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  36. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  37. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0z47hfl._.js → [root-of-the-server]__0ulzsom._.js} +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -5
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  81. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2myi4phnshkfw.js → 2r36w820aqwb9.js} +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{415k4k1rf3swg.js → 2xoclb4g7d9n7.js} +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3z3a3gb6he23i.js → 3ocvtlhgckh81.js} +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  85. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_buildManifest.js +0 -0
  86. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_clientMiddlewareManifest.js +0 -0
  87. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -15,6 +15,23 @@ function maskCode(body) {
15
15
  `).map((line) => replaceInlineSpans(line, (span) => " ".repeat(span.length))).join(`
16
16
  `);
17
17
  }
18
+ var DETAILS_OPEN = /<details\b[^>]*>/gi;
19
+ var DETAILS_CLOSE = /<\/details\s*>/gi;
20
+ function maskDetailsBlocks(body) {
21
+ const fill = (line) => " ".repeat(line.length);
22
+ const lines = body.split(`
23
+ `);
24
+ let depth = 0;
25
+ const out = lines.map((line) => {
26
+ const opens = line.match(DETAILS_OPEN)?.length ?? 0;
27
+ const closes = line.match(DETAILS_CLOSE)?.length ?? 0;
28
+ const maskThisLine = depth > 0 || opens > 0;
29
+ depth = Math.max(0, depth + opens - closes);
30
+ return maskThisLine ? fill(line) : line;
31
+ });
32
+ return out.join(`
33
+ `);
34
+ }
18
35
  function stripCode(body, options = {}) {
19
36
  const normalised = body.replace(/\r\n?/g, `
20
37
  `);
@@ -281,6 +298,9 @@ function entry(key) {
281
298
  function label(key) {
282
299
  return entry(key).id;
283
300
  }
301
+ function trancheLabel(slug) {
302
+ return `${label("tranche")}${slug}`;
303
+ }
284
304
  function matchesLabel(key, name) {
285
305
  const l = entry(key);
286
306
  if (l.form === "prefix")
@@ -1205,6 +1225,104 @@ function findDeadBranchPushes(facts) {
1205
1225
  latestCommitAt
1206
1226
  }));
1207
1227
  }
1228
+ // ../../packages/aeg-core/src/milestone-validation.ts
1229
+ var RELEASE_FIELD = /^\s*(?:\*\*)?Release(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
1230
+ var VALUE_EDGE_MARKUP2 = /^[`.;\s]+|[`.;\s]+$/g;
1231
+ function unwrapValue2(raw) {
1232
+ return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP2, "");
1233
+ }
1234
+ var RELEASE_VALUE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
1235
+ function pathText(body) {
1236
+ return stripCode(body, { inlineSpans: "keep" });
1237
+ }
1238
+ function releaseFieldFromText(text) {
1239
+ const m = text.match(RELEASE_FIELD);
1240
+ if (!m)
1241
+ return { declared: false, value: null };
1242
+ const raw = unwrapValue2(m[1] ?? "");
1243
+ return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
1244
+ }
1245
+ var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
1246
+ var NEXT_HEADING = /^#{1,6}\s+\S/m;
1247
+ var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
1248
+ function intentsBlock(text) {
1249
+ const start = text.match(INTENTS_HEADING);
1250
+ if (!start || start.index === undefined)
1251
+ return null;
1252
+ const rest = text.slice(start.index + start[0].length);
1253
+ const next = rest.match(NEXT_HEADING);
1254
+ const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
1255
+ return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
1256
+ }
1257
+ function intentsSectionText(text) {
1258
+ return intentsBlock(text)?.section ?? null;
1259
+ }
1260
+ function parseIntents(text) {
1261
+ const section = intentsSectionText(text);
1262
+ if (section === null)
1263
+ return { intents: [], malformed: false };
1264
+ const intents = [];
1265
+ for (const line of section.split(`
1266
+ `)) {
1267
+ const trimmed = line.trim();
1268
+ if (trimmed.length === 0)
1269
+ continue;
1270
+ const m = trimmed.match(INTENT_BULLET);
1271
+ if (!m)
1272
+ return { intents: [], malformed: true };
1273
+ intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
1274
+ }
1275
+ return { intents, malformed: false };
1276
+ }
1277
+ function goalRemainder(text) {
1278
+ const withoutRelease = text.replace(RELEASE_FIELD, "");
1279
+ const block = intentsBlock(withoutRelease);
1280
+ const remainder = block ? withoutRelease.slice(0, block.start) + withoutRelease.slice(block.end) : withoutRelease;
1281
+ return remainder.trim();
1282
+ }
1283
+ function checkMilestoneShape(body) {
1284
+ const errors = [];
1285
+ const text = pathText(body);
1286
+ const release = releaseFieldFromText(text);
1287
+ if (release.declared && release.value === null) {
1288
+ errors.push("milestone-validation Release: the `Release:` field is present but is not a version — " + "write a semver value (e.g. `Release: 1.2.0`), or omit the field entirely for a milestone that declares no version.");
1289
+ }
1290
+ const { intents, malformed } = parseIntents(text);
1291
+ if (malformed) {
1292
+ errors.push("milestone-validation intents: the `### Tranche intents` section does not parse — every non-blank " + "line under it must read `- <slug>: <intent text>`.");
1293
+ }
1294
+ const goal = goalRemainder(text);
1295
+ if (goal.length === 0) {
1296
+ errors.push("milestone-validation goal: no goal text — a milestone description must carry prose beyond its " + "`Release:` field and `### Tranche intents` section.");
1297
+ }
1298
+ if (errors.length > 0)
1299
+ return { status: "fail", errors };
1300
+ return { status: "pass", goal, release: release.value, intents };
1301
+ }
1302
+ function checkAdoptable(facts) {
1303
+ const errors = [];
1304
+ if (!facts.target.exists) {
1305
+ errors.push(`milestone-adopt target: Milestone "${facts.target.title}" does not exist — create it first with \`vinaya milestone create\`.`);
1306
+ } else if (facts.target.state === "closed") {
1307
+ errors.push(`milestone-adopt target: Milestone "${facts.target.title}" is closed — adopt requires an open target.`);
1308
+ }
1309
+ for (const s of facts.slugs) {
1310
+ if (!s.labelExists) {
1311
+ errors.push(`milestone-adopt unknown-slug: no \`vinaya/tranche:${s.slug}\` label exists in this repo.`);
1312
+ continue;
1313
+ }
1314
+ if (s.issueNumbers.length === 0) {
1315
+ errors.push(`milestone-adopt no-issues: \`vinaya/tranche:${s.slug}\` carries no Issues — nothing to adopt.`);
1316
+ continue;
1317
+ }
1318
+ if (s.adoptedElsewhere.length > 0) {
1319
+ errors.push(`milestone-adopt already-adopted: "${s.slug}" is already adopted into "${s.adoptedElsewhere.join('", "')}" — adopt it from there, not from here.`);
1320
+ }
1321
+ }
1322
+ if (errors.length > 0)
1323
+ return { status: "fail", errors };
1324
+ return { status: "pass" };
1325
+ }
1208
1326
  // src/lib/detect.ts
1209
1327
  import { execFile as execFile4 } from "node:child_process";
1210
1328
  import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
@@ -2117,7 +2235,8 @@ var BRIEF_BUILTINS = [
2117
2235
  "for",
2118
2236
  "closesN",
2119
2237
  "premiseCoverage",
2120
- "issueRationale"
2238
+ "issueRationale",
2239
+ "milestoneShape"
2121
2240
  ];
2122
2241
  var BriefSectionSchema = z.union([
2123
2242
  z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
@@ -2128,6 +2247,7 @@ var BriefSectionSchema = z.union([
2128
2247
  var BriefSchemaSchema = z.object({
2129
2248
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2130
2249
  issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2250
+ milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2131
2251
  ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
2132
2252
  });
2133
2253
  var MANAGED_MANIFEST_VERSION = 2;
@@ -3041,6 +3161,14 @@ function parseTrancheArgs(args) {
3041
3161
  const slug = args.find((a) => !a.startsWith("--")) ?? null;
3042
3162
  return { slug, yes };
3043
3163
  }
3164
+ function trancheArchivalStatus(issues) {
3165
+ if (issues.length === 0)
3166
+ return { kind: "no-tranche" };
3167
+ const openIssues = issues.filter((i) => i.state === "OPEN");
3168
+ if (openIssues.length > 0)
3169
+ return { kind: "open", openIssues };
3170
+ return { kind: "complete" };
3171
+ }
3044
3172
  async function runArchiveTranche(args, deps) {
3045
3173
  const { slug, yes } = parseTrancheArgs(args);
3046
3174
  if (!slug) {
@@ -3057,23 +3185,39 @@ async function runArchiveTranche(args, deps) {
3057
3185
  return 1;
3058
3186
  }
3059
3187
  const repoFlag = `${repo.owner}/${repo.repo}`;
3060
- const milestones = shJson(["gh", "api", `repos/${repoFlag}/milestones?state=open&per_page=100`]);
3061
- const milestone = milestones.find((m) => m.title === slug);
3062
- if (!milestone) {
3063
- console.error(`Error: no open tranche milestone named '${slug}' found in ${repoFlag}.`);
3064
- return 1;
3065
- }
3066
- const openIssues = shJson([
3188
+ const issues = shJson([
3067
3189
  "gh",
3068
- "api",
3069
- `repos/${repoFlag}/issues?milestone=${milestone.number}&state=open&per_page=100`
3190
+ "issue",
3191
+ "list",
3192
+ "-R",
3193
+ repoFlag,
3194
+ "--label",
3195
+ trancheLabel(slug),
3196
+ "--state",
3197
+ "all",
3198
+ "--json",
3199
+ "number,title,state",
3200
+ "--limit",
3201
+ "200"
3070
3202
  ]);
3071
- if (openIssues.length > 0) {
3072
- console.error(`Error: tranche '${slug}' still has ${openIssues.length} open task(s) — refusing to close:
3073
- ` + openIssues.map((i) => ` #${i.number} — ${i.title}`).join(`
3203
+ const status = trancheArchivalStatus(issues);
3204
+ if (status.kind === "no-tranche") {
3205
+ console.error(`Error: no tranche found for '${slug}' in ${repoFlag} no Issues carry ${trancheLabel(slug)}.`);
3206
+ return 1;
3207
+ }
3208
+ if (status.kind === "open") {
3209
+ console.error(`Error: tranche '${slug}' still has ${status.openIssues.length} open task(s) — refusing to close:
3210
+ ` + status.openIssues.map((i) => ` #${i.number} — ${i.title}`).join(`
3074
3211
  `));
3075
3212
  return 1;
3076
3213
  }
3214
+ const milestones = shJson(["gh", "api", `repos/${repoFlag}/milestones?state=open&per_page=100`]);
3215
+ const milestone = milestones.find((m) => m.title === slug);
3216
+ if (!milestone) {
3217
+ process.stdout.write(`Tranche '${slug}' is complete (${issues.length} task(s), all closed) — no open Milestone to close.
3218
+ `);
3219
+ return 0;
3220
+ }
3077
3221
  if (!yes) {
3078
3222
  const ok = await promptYesNo(`Close tranche '${slug}' (Milestone #${milestone.number}, all tasks closed)?`, false);
3079
3223
  closeStdin();
@@ -5500,7 +5644,11 @@ function runBuiltin(name, input) {
5500
5644
  for: () => checkForField(body),
5501
5645
  closesN: () => checkClosesN(body),
5502
5646
  premiseCoverage: () => checkPremiseCoverage(body, changedFiles2),
5503
- issueRationale: () => checkIssueRationale(body)
5647
+ issueRationale: () => checkIssueRationale(body),
5648
+ milestoneShape: () => {
5649
+ const result = checkMilestoneShape(body);
5650
+ return { errors: result.status === "fail" ? result.errors : [] };
5651
+ }
5504
5652
  };
5505
5653
  return table[name]().errors;
5506
5654
  }
@@ -5518,7 +5666,8 @@ var BUILTIN_RECOVERY = {
5518
5666
  for: "Add a `For: <model + environment>` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
5519
5667
  closesN: "Add a `Closes #<N>` reference naming the task Issue to the body, then re-run `{cmd}`.",
5520
5668
  premiseCoverage: "Add a `Premise:` assertion whose path matches a file this change touches, then re-run `{cmd}`.",
5521
- issueRationale: "Add the missing Planner-rationale field named above (every task Issue carries all eight fields), then re-run `{cmd}`."
5669
+ issueRationale: "Add the missing Planner-rationale field named above (every task Issue carries all eight fields), then re-run `{cmd}`.",
5670
+ milestoneShape: "Fix the Milestone description as named above — a goal, an optional well-formed `Release:` field, and an optional parseable `### Tranche intents` section — then re-run `{cmd}`."
5522
5671
  };
5523
5672
  function escapeRegExp(literal) {
5524
5673
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -5792,6 +5941,238 @@ function issueEditCommand(args) {
5792
5941
  runGhWrite(["issue", "edit", issueRef], ghArgs, bodyResult, json);
5793
5942
  }
5794
5943
 
5944
+ // src/commands/milestone.ts
5945
+ import { execFileSync as execFileSync11 } from "node:child_process";
5946
+ var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
5947
+ var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
5948
+ function sh3(args, input) {
5949
+ return execFileSync11(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
5950
+ }
5951
+ function shJson2(args, input) {
5952
+ return JSON.parse(sh3(args, input));
5953
+ }
5954
+ function ghErrorDetail(e) {
5955
+ const stderr = e?.stderr;
5956
+ const text = typeof stderr === "string" ? stderr : stderr?.toString();
5957
+ return (text && text.trim().length > 0 ? text : e?.message ?? "unknown error").trim();
5958
+ }
5959
+ function locateBodyOrRefuse2(args) {
5960
+ let result;
5961
+ try {
5962
+ result = locateBody(args);
5963
+ } catch (e) {
5964
+ if (e instanceof ForgeArgError) {
5965
+ refuse([makeCheckError("forge-args", e.message, `Fix the invocation, then re-run \`${RETRY_CREATE2}\`.`)]);
5966
+ }
5967
+ throw e;
5968
+ }
5969
+ if (result === null) {
5970
+ refuse([
5971
+ makeCheckError("forge-args", "`vinaya milestone create` requires a `--body-file <path>` (or `--body <text>`) carrying the milestone description.", `Add \`--body-file <path>\`, then re-run \`${RETRY_CREATE2}\`.`)
5972
+ ]);
5973
+ }
5974
+ return result;
5975
+ }
5976
+ async function milestoneCreateCommand(args) {
5977
+ const json = args.includes("--json");
5978
+ const validateOnly = args.includes("--validate-only");
5979
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
5980
+ const title = extractTitle(rest);
5981
+ if (!title) {
5982
+ refuse([
5983
+ makeCheckError("forge-args", "`vinaya milestone create` requires a `--title <title>` — the Milestone title is free text, never parsed for the version.", `Add \`--title <title>\`, then re-run \`${RETRY_CREATE2}\`.`)
5984
+ ]);
5985
+ }
5986
+ const bodyResult = locateBodyOrRefuse2(rest);
5987
+ const body = bodyResult.body;
5988
+ const shape = checkMilestoneShape(body);
5989
+ if (shape.status === "fail") {
5990
+ refuse(shape.errors.map((message) => makeCheckError("milestone-shape", message, `Fix the Milestone description, then re-run \`${RETRY_CREATE2}\`.`)));
5991
+ }
5992
+ const sections = resolveSections("milestone", RETRY_CREATE2);
5993
+ const schemaErrors = validateForgeWrite({ body, title: null, sections, changedFiles: [], retryCommand: RETRY_CREATE2 });
5994
+ if (schemaErrors.length > 0)
5995
+ refuse(schemaErrors);
5996
+ if (validateOnly) {
5997
+ if (json)
5998
+ printJson({ validated: true, written: false, command: "milestone create" });
5999
+ else
6000
+ process.stdout.write(`✓ all brief-schema gates PASS — nothing written (--validate-only).
6001
+ `);
6002
+ return;
6003
+ }
6004
+ const repo = await detectGitRepo();
6005
+ if (!repo?.owner || !repo.repo) {
6006
+ refuse([
6007
+ makeCheckError("forge-fetch", "Could not resolve a GitHub owner/repo from the `origin` remote.", "Run this command from inside a git repository whose `origin` remote points at GitHub.")
6008
+ ]);
6009
+ }
6010
+ const repoFlag = `${repo.owner}/${repo.repo}`;
6011
+ let out;
6012
+ try {
6013
+ out = sh3(["gh", "api", `repos/${repoFlag}/milestones`, "--input", "-"], JSON.stringify({ title, description: body }));
6014
+ } catch (e) {
6015
+ refuse([
6016
+ makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CREATE2}\`.`)
6017
+ ]);
6018
+ }
6019
+ const created = JSON.parse(out);
6020
+ if (json)
6021
+ printJson({ validated: true, written: true, number: created.number, url: created.html_url });
6022
+ else
6023
+ process.stdout.write(`${created.html_url}
6024
+ `);
6025
+ }
6026
+ function extractTarget(args) {
6027
+ for (let i = 0;i < args.length; i++) {
6028
+ const a = args[i];
6029
+ if (a === "--target" || a === "-m")
6030
+ return args[i + 1] ?? null;
6031
+ if (a.startsWith("--target="))
6032
+ return a.slice("--target=".length);
6033
+ }
6034
+ return null;
6035
+ }
6036
+ function extractSlugs(args) {
6037
+ const slugs = [];
6038
+ for (let i = 0;i < args.length; i++) {
6039
+ const a = args[i];
6040
+ if (a === "--slug") {
6041
+ const v = args[i + 1];
6042
+ if (v)
6043
+ slugs.push(v);
6044
+ } else if (a.startsWith("--slug=")) {
6045
+ slugs.push(a.slice("--slug=".length));
6046
+ }
6047
+ }
6048
+ return slugs;
6049
+ }
6050
+ function ghJsonOrRefuse(args, what) {
6051
+ try {
6052
+ return shJson2(args);
6053
+ } catch (e) {
6054
+ refuse([
6055
+ makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_ADOPT}\`.`)
6056
+ ]);
6057
+ }
6058
+ }
6059
+ function fetchAdoptFacts(repoFlag, target, slugs) {
6060
+ const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels");
6061
+ const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones");
6062
+ const targetMilestone = milestones.find((m) => m.title === target);
6063
+ const targetFacts = {
6064
+ title: target,
6065
+ exists: !!targetMilestone,
6066
+ state: targetMilestone?.state ?? null
6067
+ };
6068
+ const slugFacts = slugs.map((slug) => {
6069
+ const labelExists = labels.some((l) => l.name === trancheLabel(slug));
6070
+ const issues = labelExists ? ghJsonOrRefuse([
6071
+ "gh",
6072
+ "issue",
6073
+ "list",
6074
+ "-R",
6075
+ repoFlag,
6076
+ "--label",
6077
+ trancheLabel(slug),
6078
+ "--state",
6079
+ "all",
6080
+ "--json",
6081
+ "number,state,milestone",
6082
+ "--limit",
6083
+ "200"
6084
+ ], `Issues for \`${trancheLabel(slug)}\``) : [];
6085
+ const elsewhere = new Set;
6086
+ for (const issue of issues) {
6087
+ const title = issue.milestone?.title;
6088
+ if (title && title !== slug && title !== target)
6089
+ elsewhere.add(title);
6090
+ }
6091
+ return {
6092
+ slug,
6093
+ labelExists,
6094
+ issueNumbers: issues.map((i) => i.number),
6095
+ adoptedElsewhere: [...elsewhere]
6096
+ };
6097
+ });
6098
+ return { facts: { target: targetFacts, slugs: slugFacts }, milestones };
6099
+ }
6100
+ function failMidWrite(message) {
6101
+ console.error(`
6102
+ [milestone adopt] PARTIAL WRITE — ${message}`);
6103
+ console.error(`[milestone adopt] Re-running \`${RETRY_ADOPT}\` is safe — every write here is idempotent.`);
6104
+ process.exit(1);
6105
+ }
6106
+ async function milestoneAdoptCommand(args) {
6107
+ const json = args.includes("--json");
6108
+ const validateOnly = args.includes("--validate-only");
6109
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
6110
+ const target = extractTarget(rest);
6111
+ if (!target) {
6112
+ refuse([
6113
+ makeCheckError("forge-args", "`vinaya milestone adopt` requires a `--target <title>` — the Milestone every named slug is adopted into.", `Add \`--target <title>\`, then re-run \`${RETRY_ADOPT}\`.`)
6114
+ ]);
6115
+ }
6116
+ const slugs = extractSlugs(rest);
6117
+ if (slugs.length === 0) {
6118
+ refuse([
6119
+ makeCheckError("forge-args", "`vinaya milestone adopt` requires at least one `--slug <slug>` — the tranche(s) to move into the target Milestone.", `Add one or more \`--slug <slug>\`, then re-run \`${RETRY_ADOPT}\`.`)
6120
+ ]);
6121
+ }
6122
+ const repo = await detectGitRepo();
6123
+ if (!repo?.owner || !repo.repo) {
6124
+ refuse([
6125
+ makeCheckError("forge-fetch", "Could not resolve a GitHub owner/repo from the `origin` remote.", "Run this command from inside a git repository whose `origin` remote points at GitHub.")
6126
+ ]);
6127
+ }
6128
+ const repoFlag = `${repo.owner}/${repo.repo}`;
6129
+ const { facts, milestones } = fetchAdoptFacts(repoFlag, target, slugs);
6130
+ const verdict = checkAdoptable(facts);
6131
+ if (verdict.status === "fail") {
6132
+ refuse(verdict.errors.map((message) => makeCheckError("milestone-adopt", message, `Fix the input above, then re-run \`${RETRY_ADOPT}\`.`)));
6133
+ }
6134
+ if (validateOnly) {
6135
+ if (json)
6136
+ printJson({ validated: true, written: false, command: "milestone adopt" });
6137
+ else
6138
+ process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
6139
+ `);
6140
+ return;
6141
+ }
6142
+ const adopted = [];
6143
+ for (const slugFact of facts.slugs) {
6144
+ for (const issueNumber of slugFact.issueNumbers) {
6145
+ try {
6146
+ sh3(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
6147
+ } catch (e) {
6148
+ failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
6149
+ }
6150
+ }
6151
+ const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
6152
+ let closedMilestone = null;
6153
+ if (legacy) {
6154
+ closedMilestone = legacy.number;
6155
+ if (legacy.state !== "closed") {
6156
+ try {
6157
+ sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
6158
+ } catch (e) {
6159
+ failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
6160
+ }
6161
+ }
6162
+ }
6163
+ adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
6164
+ }
6165
+ if (json)
6166
+ printJson({ validated: true, written: true, target, adopted });
6167
+ else {
6168
+ for (const a of adopted) {
6169
+ const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
6170
+ process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
6171
+ `);
6172
+ }
6173
+ }
6174
+ }
6175
+
5795
6176
  // src/commands/new-check.ts
5796
6177
  import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
5797
6178
  import { join as join15 } from "node:path";
@@ -5836,12 +6217,205 @@ ${registration}
5836
6217
  }
5837
6218
 
5838
6219
  // src/commands/pr.ts
5839
- import { execFileSync as execFileSync11 } from "node:child_process";
5840
- var RETRY_CREATE2 = "vinaya pr create --validate-only …";
6220
+ import { execFileSync as execFileSync12 } from "node:child_process";
6221
+
6222
+ // src/checks/body-bare-digits-logic.ts
6223
+ function blankRange(text, start, end) {
6224
+ const region = text.slice(start, end);
6225
+ return text.slice(0, start) + region.replace(/[^\n]/g, " ") + text.slice(end);
6226
+ }
6227
+ var EXEMPT_ANCHOR_FIELDS = ["CLOSES", "TIER", "PROJECT", "EVIDENCE"];
6228
+ var FIELD_SECTION_HEADING = {
6229
+ CLOSES: null,
6230
+ PROJECT: null,
6231
+ TIER: /^scope$/i,
6232
+ EVIDENCE: /^evidence$/i
6233
+ };
6234
+ var HEADING = /^#{1,6}\s/;
6235
+ var HEADING_TEXT = /^#{1,6}\s+(.*?)\s*$/;
6236
+ function headerBlockEnd(body) {
6237
+ const lines = body.split(`
6238
+ `);
6239
+ let offset = 0;
6240
+ for (const line of lines) {
6241
+ if (HEADING.test(line))
6242
+ return offset;
6243
+ offset += line.length + 1;
6244
+ }
6245
+ return body.length;
6246
+ }
6247
+ function namedSectionBounds(body, namePattern) {
6248
+ const lines = body.split(`
6249
+ `);
6250
+ let offset = 0;
6251
+ let start = -1;
6252
+ let end = body.length;
6253
+ for (const line of lines) {
6254
+ const headingText = HEADING_TEXT.exec(line);
6255
+ if (start === -1 && headingText && namePattern.test(headingText[1])) {
6256
+ start = offset;
6257
+ } else if (start !== -1 && HEADING.test(line)) {
6258
+ end = offset;
6259
+ break;
6260
+ }
6261
+ offset += line.length + 1;
6262
+ }
6263
+ return start === -1 ? null : { start, end };
6264
+ }
6265
+ function isInCanonicalSection(body, field, outerStart) {
6266
+ const pattern = FIELD_SECTION_HEADING[field];
6267
+ if (pattern === null)
6268
+ return outerStart < headerBlockEnd(body);
6269
+ const bounds = namedSectionBounds(body, pattern);
6270
+ return bounds !== null && outerStart >= bounds.start && outerStart < bounds.end;
6271
+ }
6272
+ var PROJECT_LABEL = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?/i;
6273
+ var FIELD_CONTENT_SIGNATURE = {
6274
+ CLOSES: /Closes\s*#\d+/i,
6275
+ PROJECT: PROJECT_LABEL,
6276
+ TIER: TIER_FIELD,
6277
+ EVIDENCE: /^Head:\s*\S/m
6278
+ };
6279
+ function blankAnchoredRegions(body) {
6280
+ let masked = body;
6281
+ for (const field of EXEMPT_ANCHOR_FIELDS) {
6282
+ const bounds = anchoredRegionBounds(body, field);
6283
+ if (!bounds || !isInCanonicalSection(body, field, bounds.outerStart))
6284
+ continue;
6285
+ const content = body.slice(bounds.innerStart, bounds.innerEnd);
6286
+ if (!FIELD_CONTENT_SIGNATURE[field].test(content))
6287
+ continue;
6288
+ masked = blankRange(masked, bounds.outerStart, bounds.innerStart);
6289
+ masked = blankRange(masked, bounds.innerEnd, bounds.outerEnd);
6290
+ const blankedContent = content.split(`
6291
+ `).map(BOUNDED_ANCHOR_BLANK[field]).join(`
6292
+ `);
6293
+ masked = masked.slice(0, bounds.innerStart) + blankedContent + masked.slice(bounds.innerEnd);
6294
+ }
6295
+ return masked;
6296
+ }
6297
+ function blankTokenReportSection(body) {
6298
+ const lines = body.split(`
6299
+ `);
6300
+ const HEADING2 = /^#{1,6}\s/;
6301
+ const TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
6302
+ const startIdx = lines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
6303
+ if (startIdx === -1)
6304
+ return body;
6305
+ let endIdx = lines.length;
6306
+ for (let i = startIdx + 1;i < lines.length; i++) {
6307
+ if (HEADING2.test(lines[i])) {
6308
+ endIdx = i;
6309
+ break;
6310
+ }
6311
+ }
6312
+ const fill = (line) => " ".repeat(line.length);
6313
+ for (let i = startIdx;i < endIdx; i++)
6314
+ lines[i] = fill(lines[i]);
6315
+ return lines.join(`
6316
+ `);
6317
+ }
6318
+ function blankTierField(line) {
6319
+ const m = TIER_FIELD.exec(line);
6320
+ if (!m)
6321
+ return line;
6322
+ return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
6323
+ }
6324
+ var SEGMENT_HAS_DIGIT = /\p{Nd}/u;
6325
+ function blankProjectField(line) {
6326
+ const label2 = PROJECT_LABEL.exec(line);
6327
+ if (!label2)
6328
+ return line;
6329
+ const valueStart = label2.index + label2[0].length;
6330
+ const segments = line.slice(valueStart).split(",");
6331
+ const blanked = segments.map((seg) => {
6332
+ const name = unwrapValue(seg);
6333
+ const isRealName = PROJECT_SLUG.test(name) && !SEGMENT_HAS_DIGIT.test(name);
6334
+ return isRealName ? " ".repeat(seg.length) : seg;
6335
+ });
6336
+ return line.slice(0, valueStart) + blanked.join(",");
6337
+ }
6338
+ function blankUnanchoredStructuralFields(body) {
6339
+ return body.split(`
6340
+ `).map((l) => blankProjectField(blankTierField(l))).join(`
6341
+ `);
6342
+ }
6343
+ var CLOSES_REF = /Closes\s*#\d+/i;
6344
+ var EVIDENCE_HEADING = /^#{1,6}\s/;
6345
+ var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
6346
+ function blankClosesField(line) {
6347
+ const m = CLOSES_REF.exec(line);
6348
+ if (!m)
6349
+ return line;
6350
+ return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
6351
+ }
6352
+ function blankEvidenceField(line) {
6353
+ const trimmed = line.trim();
6354
+ if (trimmed === "" || EVIDENCE_HEAD_LINE.test(trimmed) || EVIDENCE_HEADING.test(trimmed))
6355
+ return " ".repeat(line.length);
6356
+ return line;
6357
+ }
6358
+ var BOUNDED_ANCHOR_BLANK = {
6359
+ CLOSES: blankClosesField,
6360
+ TIER: blankTierField,
6361
+ PROJECT: blankProjectField,
6362
+ EVIDENCE: blankEvidenceField
6363
+ };
6364
+ function buildScanMask(body) {
6365
+ let masked = maskDetailsBlocks(maskCode(body));
6366
+ masked = blankAnchoredRegions(masked);
6367
+ masked = blankTokenReportSection(masked);
6368
+ masked = blankUnanchoredStructuralFields(masked);
6369
+ return masked;
6370
+ }
6371
+ var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
6372
+ var LIST_MARKER_TOKEN = /^\p{Nd}{1,9}[.)]$/u;
6373
+ var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
6374
+ var HTML_ENTITIES = {
6375
+ "&quot;": '"',
6376
+ "&apos;": "'",
6377
+ "&lt;": "<",
6378
+ "&gt;": ">",
6379
+ "&amp;": "&"
6380
+ };
6381
+ function decodeNamedEntities(body) {
6382
+ return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
6383
+ }
6384
+ function isLineLeadingListMarker(line, matchStart, rawToken) {
6385
+ if (!LIST_MARKER_TOKEN.test(rawToken))
6386
+ return false;
6387
+ const before = line.slice(0, matchStart);
6388
+ return /^ {0,3}$/.test(before);
6389
+ }
6390
+ function checkBareDigits(rawBody) {
6391
+ const body = decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
6392
+ const masked = buildScanMask(body);
6393
+ const maskedLines = masked.split(`
6394
+ `);
6395
+ const origLines = body.split(`
6396
+ `);
6397
+ const violations = [];
6398
+ for (let i = 0;i < maskedLines.length; i++) {
6399
+ const maskedLine = maskedLines[i];
6400
+ const origLine = origLines[i];
6401
+ TOKEN_WITH_DIGIT.lastIndex = 0;
6402
+ let m;
6403
+ while ((m = TOKEN_WITH_DIGIT.exec(maskedLine)) !== null) {
6404
+ const rawToken = origLine.slice(m.index, m.index + m[0].length);
6405
+ if (isLineLeadingListMarker(maskedLine, m.index, rawToken))
6406
+ continue;
6407
+ violations.push({ line: i + 1, text: origLine.trim() });
6408
+ }
6409
+ }
6410
+ return { violations };
6411
+ }
6412
+
6413
+ // src/commands/pr.ts
6414
+ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
5841
6415
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
5842
6416
  function git3(args) {
5843
6417
  try {
5844
- return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6418
+ return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5845
6419
  } catch {
5846
6420
  return "";
5847
6421
  }
@@ -5854,7 +6428,7 @@ function localChangedFiles() {
5854
6428
  return out.split(`
5855
6429
  `).map((s) => s.trim()).filter(Boolean);
5856
6430
  }
5857
- function locateBodyOrRefuse2(ghArgs, retryCommand) {
6431
+ function locateBodyOrRefuse3(ghArgs, retryCommand) {
5858
6432
  try {
5859
6433
  return locateBody(ghArgs);
5860
6434
  } catch (e) {
@@ -5875,7 +6449,7 @@ function reportPass2(json, command) {
5875
6449
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5876
6450
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5877
6451
  try {
5878
- const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
6452
+ const out = execFileSync12("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5879
6453
  const url = out.trim();
5880
6454
  if (json)
5881
6455
  printJson({ validated: true, written: true, url });
@@ -5889,7 +6463,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5889
6463
  function fetchPrForgeContext(prRef) {
5890
6464
  let viewOut;
5891
6465
  try {
5892
- viewOut = execFileSync11("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
6466
+ viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5893
6467
  encoding: "utf8",
5894
6468
  stdio: ["ignore", "pipe", "pipe"]
5895
6469
  });
@@ -5914,19 +6488,25 @@ function fetchPrForgeContext(prRef) {
5914
6488
  const changedFiles2 = (parsed.files ?? []).map((f) => f.path);
5915
6489
  return { changedFiles: changedFiles2, branch: parsed.headRefName ?? "" };
5916
6490
  }
6491
+ function refuseOnBareDigits(body, retryCommand) {
6492
+ const { violations } = checkBareDigits(body);
6493
+ if (violations.length === 0)
6494
+ return;
6495
+ refuse(violations.map((v) => makeCheckError("body-bare-digits", `body-bare-digits: bare digit outside a fenced block, line ${v.line}: ${v.text}`, `Backtick the digit, move it into a fenced block, or state it as a symbol, then re-run \`${retryCommand}\`.`)));
6496
+ }
5917
6497
  function prCreateCommand(args) {
5918
6498
  const json = args.includes("--json");
5919
6499
  const validateOnly = args.includes("--validate-only");
5920
6500
  const ghArgs = args.filter((a) => a !== "--json" && a !== "--validate-only");
5921
- const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_CREATE2);
6501
+ const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_CREATE3);
5922
6502
  const body = bodyResult?.body ?? null;
5923
6503
  const title = extractTitle(ghArgs);
5924
6504
  if (body === null) {
5925
6505
  refuse([
5926
- makeCheckError("forge-args", "No `--body-file <path>` (or `--body`) argument found — the brief-schema gate needs the PR body to validate it.", `Add \`--body-file <path>\` (or \`--body\`), then re-run \`${RETRY_CREATE2}\`.`)
6506
+ makeCheckError("forge-args", "No `--body-file <path>` (or `--body`) argument found — the brief-schema gate needs the PR body to validate it.", `Add \`--body-file <path>\` (or \`--body\`), then re-run \`${RETRY_CREATE3}\`.`)
5927
6507
  ]);
5928
6508
  }
5929
- const sections = resolveSections("pr", RETRY_CREATE2);
6509
+ const sections = resolveSections("pr", RETRY_CREATE3);
5930
6510
  const changedFiles2 = localChangedFiles();
5931
6511
  const headCommit = git3(["rev-parse", "--verify", "--quiet", "HEAD"]);
5932
6512
  const branch = headCommit === "" ? "" : git3(["symbolic-ref", "--quiet", "--short", "HEAD"]);
@@ -5935,11 +6515,12 @@ function prCreateCommand(args) {
5935
6515
  title,
5936
6516
  sections,
5937
6517
  changedFiles: changedFiles2,
5938
- retryCommand: RETRY_CREATE2,
6518
+ retryCommand: RETRY_CREATE3,
5939
6519
  branch
5940
6520
  });
5941
6521
  if (errors.length > 0)
5942
6522
  refuse(errors);
6523
+ refuseOnBareDigits(body, RETRY_CREATE3);
5943
6524
  if (validateOnly) {
5944
6525
  reportPass2(json, "pr create");
5945
6526
  return;
@@ -5957,7 +6538,7 @@ function prEditCommand(args) {
5957
6538
  ]);
5958
6539
  }
5959
6540
  const ghArgs = rest.slice(1);
5960
- const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_EDIT2);
6541
+ const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_EDIT2);
5961
6542
  const body = bodyResult?.body ?? null;
5962
6543
  const title = extractTitle(ghArgs);
5963
6544
  if (body === null && title === null) {
@@ -5983,6 +6564,8 @@ function prEditCommand(args) {
5983
6564
  });
5984
6565
  if (errors.length > 0)
5985
6566
  refuse(errors);
6567
+ if (body !== null)
6568
+ refuseOnBareDigits(body, RETRY_EDIT2);
5986
6569
  if (validateOnly) {
5987
6570
  reportPass2(json, "pr edit");
5988
6571
  return;
@@ -5991,14 +6574,14 @@ function prEditCommand(args) {
5991
6574
  }
5992
6575
 
5993
6576
  // src/commands/pr-report.ts
5994
- import { execFileSync as execFileSync12, spawnSync as spawnSync2 } from "node:child_process";
6577
+ import { execFileSync as execFileSync13, spawnSync as spawnSync2 } from "node:child_process";
5995
6578
  import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
5996
6579
  import { join as join16 } from "node:path";
5997
6580
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
5998
6581
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
5999
6582
  function git4(args) {
6000
6583
  try {
6001
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6584
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6002
6585
  } catch {
6003
6586
  return "";
6004
6587
  }
@@ -6016,7 +6599,7 @@ class GitCommandError extends Error {
6016
6599
  }
6017
6600
  function gitStrict(args) {
6018
6601
  try {
6019
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6602
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6020
6603
  } catch (err) {
6021
6604
  const stderr = err.stderr;
6022
6605
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -6170,7 +6753,7 @@ async function prReportCommand(args) {
6170
6753
  }
6171
6754
 
6172
6755
  // src/commands/quickstart.ts
6173
- import { execFileSync as execFileSync13 } from "node:child_process";
6756
+ import { execFileSync as execFileSync14 } from "node:child_process";
6174
6757
  import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
6175
6758
  import { join as join18 } from "node:path";
6176
6759
 
@@ -6366,7 +6949,7 @@ function realDeps6() {
6366
6949
  };
6367
6950
  }
6368
6951
  function execGit(repoRoot2, args) {
6369
- return execFileSync13("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6952
+ return execFileSync14("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6370
6953
  }
6371
6954
  function errorDetail(err) {
6372
6955
  const stderr = err.stderr;
@@ -6483,7 +7066,7 @@ async function quickstartCommand(args) {
6483
7066
  }
6484
7067
 
6485
7068
  // src/commands/review-post.ts
6486
- import { execFileSync as execFileSync14 } from "node:child_process";
7069
+ import { execFileSync as execFileSync15 } from "node:child_process";
6487
7070
  import { readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
6488
7071
  import { tmpdir as tmpdir2 } from "node:os";
6489
7072
  import { join as join19 } from "node:path";
@@ -6690,7 +7273,7 @@ function normalizeSecurityVerdict(raw) {
6690
7273
  }
6691
7274
  function gh(args) {
6692
7275
  try {
6693
- return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
7276
+ return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6694
7277
  } catch (err) {
6695
7278
  const stderr = err.stderr;
6696
7279
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -7402,12 +7985,12 @@ async function upgradeCommand(args) {
7402
7985
  }
7403
7986
 
7404
7987
  // src/commands/waiver.ts
7405
- import { execFileSync as execFileSync15 } from "node:child_process";
7988
+ import { execFileSync as execFileSync16 } from "node:child_process";
7406
7989
  function labelFor(kind) {
7407
7990
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
7408
7991
  }
7409
7992
  function gh2(args) {
7410
- return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
7993
+ return execFileSync16("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
7411
7994
  }
7412
7995
  function detectPrNumber() {
7413
7996
  try {
@@ -7507,8 +8090,8 @@ Reason: ${reason}`];
7507
8090
  }
7508
8091
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
7509
8092
  `);
7510
- execFileSync15("gh", editArgs, { stdio: "inherit", env: process.env });
7511
- execFileSync15("gh", commentArgs, { stdio: "inherit", env: process.env });
8093
+ execFileSync16("gh", editArgs, { stdio: "inherit", env: process.env });
8094
+ execFileSync16("gh", commentArgs, { stdio: "inherit", env: process.env });
7512
8095
  process.stdout.write(`
7513
8096
  ✓ Label applied and reason posted as a PR comment.
7514
8097
  `);
@@ -7668,6 +8251,37 @@ var COMMANDS = [
7668
8251
  ],
7669
8252
  status: "shipped"
7670
8253
  },
8254
+ {
8255
+ name: "milestone create",
8256
+ description: "Create a GitHub Milestone from a validated body",
8257
+ flags: [
8258
+ { flag: "--title", description: "Milestone title — free text, never parsed for a version" },
8259
+ {
8260
+ flag: "--body-file",
8261
+ description: "Path to the Milestone description (stream-safe; same bytes validated and sent)"
8262
+ },
8263
+ { flag: "--validate-only", description: "Run every gate and report PASS without creating the Milestone" },
8264
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
8265
+ ],
8266
+ details: [
8267
+ "Refuses before any `gh` call when the goal is absent, an optional `Release:` field is present but not a version, or an optional `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `Release:` is the sole authority for the milestone's version — the title is never parsed for one."
8268
+ ],
8269
+ status: "shipped"
8270
+ },
8271
+ {
8272
+ name: "milestone adopt",
8273
+ description: "Move one or more existing tranches into a target Milestone",
8274
+ flags: [
8275
+ { flag: "--target", description: "The Milestone every named slug is adopted into" },
8276
+ { flag: "--slug", description: "A tranche slug to adopt — repeatable for a multi-slug move" },
8277
+ { flag: "--validate-only", description: "Run every gate and report PASS without writing anything" },
8278
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
8279
+ ],
8280
+ details: [
8281
+ "Reattaches every Issue carrying each named `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact for every named slug is gathered and checked in ONE call before any write, so a single unsafe slug — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone — refuses the whole invocation, not just its own slug."
8282
+ ],
8283
+ status: "shipped"
8284
+ },
7671
8285
  {
7672
8286
  name: "review post",
7673
8287
  description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
@@ -7966,6 +8580,18 @@ try {
7966
8580
  }
7967
8581
  break;
7968
8582
  }
8583
+ case "milestone": {
8584
+ const [subcommand, ...rest] = args;
8585
+ if (subcommand === "create") {
8586
+ await milestoneCreateCommand(rest);
8587
+ } else if (subcommand === "adopt") {
8588
+ await milestoneAdoptCommand(rest);
8589
+ } else {
8590
+ console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected 'create' or 'adopt')`);
8591
+ process.exit(2);
8592
+ }
8593
+ break;
8594
+ }
7969
8595
  case "review": {
7970
8596
  const [subcommand, ...rest] = args;
7971
8597
  if (subcommand === "post") {