@attalabs/vinaya 0.17.1 → 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 +37 -1
  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 +62 -14
  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 +62 -14
  11. package/dist/checks/bin/check-dead-branch-push.js +57 -12
  12. package/dist/checks/bin/check-dispatch-readiness.js +129 -41
  13. package/dist/checks/bin/check-doc-coverage-push.js +62 -14
  14. package/dist/checks/bin/check-doc-coverage.js +62 -14
  15. package/dist/checks/bin/check-evidence-fresh.js +57 -12
  16. package/dist/checks/bin/check-first-push-dispatch.js +130 -42
  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 +62 -14
  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 +889 -109
  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]__1ccgndq._.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/{40x7f5hm7a5tk.js → 2r36w820aqwb9.js} +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ojfbn3yvy-8_.js → 2xoclb4g7d9n7.js} +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0l_vubs1ephe5.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/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_buildManifest.js +0 -0
  86. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_clientMiddlewareManifest.js +0 -0
  87. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → 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";
@@ -2025,18 +2143,6 @@ function buildInitOps(ctx) {
2025
2143
  ops.push({ kind: "print", message: CODEOWNERS_NOTE, group: "Branch protection (printed, never applied)" });
2026
2144
  return ops;
2027
2145
  }
2028
- function buildInitProductOps(name) {
2029
- const safe = name.trim();
2030
- return [
2031
- {
2032
- kind: "create-label",
2033
- name: `project:${safe}`,
2034
- color: "0e8a16",
2035
- description: `Governed product area: ${safe}`,
2036
- group: `Governed product area: ${safe}`
2037
- }
2038
- ];
2039
- }
2040
2146
 
2041
2147
  // src/lib/ops.ts
2042
2148
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -2129,7 +2235,8 @@ var BRIEF_BUILTINS = [
2129
2235
  "for",
2130
2236
  "closesN",
2131
2237
  "premiseCoverage",
2132
- "issueRationale"
2238
+ "issueRationale",
2239
+ "milestoneShape"
2133
2240
  ];
2134
2241
  var BriefSectionSchema = z.union([
2135
2242
  z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
@@ -2139,7 +2246,9 @@ var BriefSectionSchema = z.union([
2139
2246
  ]);
2140
2247
  var BriefSchemaSchema = z.object({
2141
2248
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2142
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
2249
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2250
+ milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2251
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
2143
2252
  });
2144
2253
  var MANAGED_MANIFEST_VERSION = 2;
2145
2254
  function isSafeRepoRelPath(p) {
@@ -2343,17 +2452,22 @@ ${end}`;
2343
2452
  function abs(repoRoot, relPath) {
2344
2453
  return join4(repoRoot, relPath);
2345
2454
  }
2455
+ var GIT_DIR_PREFIX = ".git/";
2346
2456
  function resolveManagedBlockPath(repoRoot, opPath) {
2347
- if (!opPath.startsWith(".git/"))
2457
+ if (!opPath.startsWith(GIT_DIR_PREFIX))
2348
2458
  return join4(repoRoot, opPath);
2459
+ return join4(gitCommonDir(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
2460
+ }
2461
+ function gitCommonDir(repoRoot) {
2349
2462
  try {
2350
2463
  const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
2351
2464
  encoding: "utf8"
2352
2465
  }).trim();
2353
- const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
2354
- return join4(gitDir, opPath.slice(".git/".length));
2466
+ if (commonDir === "")
2467
+ return join4(repoRoot, ".git");
2468
+ return isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
2355
2469
  } catch {
2356
- return join4(repoRoot, opPath);
2470
+ return join4(repoRoot, ".git");
2357
2471
  }
2358
2472
  }
2359
2473
  function containedAbs(repoRoot, relPath) {
@@ -2363,6 +2477,16 @@ function containedAbs(repoRoot, relPath) {
2363
2477
  return null;
2364
2478
  return target === root || target.startsWith(root + sep) ? target : null;
2365
2479
  }
2480
+ function containedManagedBlockAbs(repoRoot, relPath) {
2481
+ if (!relPath.startsWith(GIT_DIR_PREFIX))
2482
+ return containedAbs(repoRoot, relPath);
2483
+ const commonDir = gitCommonDir(repoRoot);
2484
+ const hooksRoot = resolve(join4(commonDir, "hooks"));
2485
+ const target = resolve(join4(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
2486
+ if (target === hooksRoot)
2487
+ return null;
2488
+ return target.startsWith(hooksRoot + sep) ? target : null;
2489
+ }
2366
2490
  function fileContains(repoRoot, relPath, needle) {
2367
2491
  const p = resolveManagedBlockPath(repoRoot, relPath);
2368
2492
  if (!existsSync3(p))
@@ -2583,7 +2707,7 @@ function planEject(manifest, repoRoot) {
2583
2707
  const actions = [];
2584
2708
  const escapes = [];
2585
2709
  for (const b of manifest.blocks) {
2586
- const p = containedAbs(repoRoot, b.path);
2710
+ const p = containedManagedBlockAbs(repoRoot, b.path);
2587
2711
  if (p === null) {
2588
2712
  escapes.push(b.path);
2589
2713
  continue;
@@ -2648,7 +2772,7 @@ function applyEject(plan, repoRoot) {
2648
2772
  if (a.kind === "strip-block") {
2649
2773
  if (!a.present)
2650
2774
  continue;
2651
- const p = containedAbs(repoRoot, a.path);
2775
+ const p = containedManagedBlockAbs(repoRoot, a.path);
2652
2776
  if (p === null)
2653
2777
  continue;
2654
2778
  const content = readFileSync3(p, "utf-8");
@@ -3037,6 +3161,14 @@ function parseTrancheArgs(args) {
3037
3161
  const slug = args.find((a) => !a.startsWith("--")) ?? null;
3038
3162
  return { slug, yes };
3039
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
+ }
3040
3172
  async function runArchiveTranche(args, deps) {
3041
3173
  const { slug, yes } = parseTrancheArgs(args);
3042
3174
  if (!slug) {
@@ -3053,23 +3185,39 @@ async function runArchiveTranche(args, deps) {
3053
3185
  return 1;
3054
3186
  }
3055
3187
  const repoFlag = `${repo.owner}/${repo.repo}`;
3056
- const milestones = shJson(["gh", "api", `repos/${repoFlag}/milestones?state=open&per_page=100`]);
3057
- const milestone = milestones.find((m) => m.title === slug);
3058
- if (!milestone) {
3059
- console.error(`Error: no open tranche milestone named '${slug}' found in ${repoFlag}.`);
3060
- return 1;
3061
- }
3062
- const openIssues = shJson([
3188
+ const issues = shJson([
3063
3189
  "gh",
3064
- "api",
3065
- `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"
3066
3202
  ]);
3067
- if (openIssues.length > 0) {
3068
- console.error(`Error: tranche '${slug}' still has ${openIssues.length} open task(s) — refusing to close:
3069
- ` + 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(`
3070
3211
  `));
3071
3212
  return 1;
3072
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
+ }
3073
3221
  if (!yes) {
3074
3222
  const ok = await promptYesNo(`Close tranche '${slug}' (Milestone #${milestone.number}, all tasks closed)?`, false);
3075
3223
  closeStdin();
@@ -4735,6 +4883,31 @@ function diagnoseBlastRadiusDeprecation(repoRoot, config) {
4735
4883
  warn("blast-radius", `${LEGACY_AEG_PACKAGES_PATH} is deprecated — checkBlastRadiusScope now derives packages/* domains live and ships built-in cross-cutting defaults. Declare anything beyond those in vinaya.config.json's blastRadius.extraDomains instead. ${migrationNote}`)
4736
4884
  ];
4737
4885
  }
4886
+ var BRIEF_KIND_LABEL = {
4887
+ pr: "PR bodies",
4888
+ issue: "task Issue bodies"
4889
+ };
4890
+ function builtinsIn(sections) {
4891
+ return (sections ?? []).flatMap((s) => ("builtin" in s) ? [s.builtin] : []);
4892
+ }
4893
+ function diagnoseBriefSchemaDrift(config) {
4894
+ if (!config)
4895
+ return [];
4896
+ const shipped = starterConfig().briefSchema;
4897
+ const acked = new Set(config.briefSchema?.ack ?? []);
4898
+ const findings = [];
4899
+ for (const kind of ["pr", "issue"]) {
4900
+ const expected = builtinsIn(shipped?.[kind]?.sections);
4901
+ if (expected.length === 0)
4902
+ continue;
4903
+ const present = new Set(builtinsIn(config.briefSchema?.[kind]?.sections));
4904
+ const missing = expected.filter((b) => !present.has(b) && !acked.has(b));
4905
+ if (missing.length === 0)
4906
+ continue;
4907
+ findings.push(info("brief-schema", `briefSchema.${kind} is missing ${missing.length} builtin${missing.length === 1 ? "" : "s"} the shipped default declares for ${BRIEF_KIND_LABEL[kind]}: ${missing.join(", ")}. ` + "This is adopter-owned config — `vinaya upgrade` will never restore it, and nothing else reports it. " + "If the omission is deliberate, list those names in `briefSchema.ack` to silence this; if it was a workaround for a vinaya defect, check whether that defect is fixed and the builtin can come back."));
4908
+ }
4909
+ return findings;
4910
+ }
4738
4911
  function diagnoseEnvDeclarations(repoRoot, config) {
4739
4912
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
4740
4913
  const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
@@ -4893,6 +5066,7 @@ async function runDoctor(args, deps) {
4893
5066
  findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
4894
5067
  }
4895
5068
  findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
5069
+ findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
4896
5070
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4897
5071
  findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
4898
5072
  findings.push(...diagnoseGlobalConfigChecks());
@@ -5034,7 +5208,7 @@ async function runEject(args, deps) {
5034
5208
  const plan = planEject(read.manifest, repo.repoRoot);
5035
5209
  const unarm = read.manifest.blocks.some((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)) && await deps.readHooksPath(repo.repoRoot) === TRACKED_HOOK_DIR;
5036
5210
  if (plan.escapes.length > 0) {
5037
- console.error("Error: the ownership manifest records paths that resolve OUTSIDE this repo:");
5211
+ console.error("Error: the ownership manifest records paths that resolve OUTSIDE the bounds vinaya may touch:");
5038
5212
  for (const p of plan.escapes)
5039
5213
  console.error(` ${p}`);
5040
5214
  console.error("Refusing to remove anything. This manifest is corrupt or hand-edited — fix vinaya.config.json, then re-run.");
@@ -5315,17 +5489,9 @@ async function runInitProduct(args, deps) {
5315
5489
  console.error("Error: this repo is not Vinaya-initialized yet. Run `vinaya init` first.");
5316
5490
  return 1;
5317
5491
  }
5318
- const noRemote = !repo.owner || !repo.repo;
5319
- if (noRemote) {
5320
- console.warn("Warning: no `origin` remote (or it is not a GitHub URL) — skipping label creation. " + `Re-run 'vinaya init product ${name}' after adding a GitHub remote to create it.`);
5321
- }
5322
- const ops = noRemote ? [] : buildInitProductOps(name);
5323
- const plan = planInstall(ops, repo.repoRoot, new Set(existing.files));
5324
5492
  const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
5325
5493
  process.stdout.write(`vinaya init product ${name} — the full diff:
5326
5494
 
5327
- `);
5328
- process.stdout.write(`${renderInstallDiff(plan)}
5329
5495
  `);
5330
5496
  process.stdout.write(`── Project registry ─────────────────────────────
5331
5497
  `);
@@ -5346,29 +5512,11 @@ async function runInitProduct(args, deps) {
5346
5512
  }
5347
5513
  }
5348
5514
  applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
5349
- const added = await applyInstall(plan, repo.repoRoot, deps.labelGateway(repo.repoRoot));
5350
- const merged = {
5351
- version: existing.version,
5352
- files: [...new Set([...existing.files, ...added.files])],
5353
- blocks: dedupeBlocks2([...existing.blocks, ...added.blocks]),
5354
- labels: [...new Set([...existing.labels, ...added.labels])]
5355
- };
5356
- writeManifest(repo.repoRoot, merged);
5357
5515
  process.stdout.write(`
5358
5516
  Governed product area '${name}' scaffolded.
5359
5517
  `);
5360
5518
  return 0;
5361
5519
  }
5362
- function dedupeBlocks2(bs) {
5363
- const seen = new Set;
5364
- return bs.filter((b) => {
5365
- const k = `${b.path}::${b.marker}`;
5366
- if (seen.has(k))
5367
- return false;
5368
- seen.add(k);
5369
- return true;
5370
- });
5371
- }
5372
5520
  async function initCommand(args) {
5373
5521
  process.exit(await runInit(args, realDeps5()));
5374
5522
  }
@@ -5496,7 +5644,11 @@ function runBuiltin(name, input) {
5496
5644
  for: () => checkForField(body),
5497
5645
  closesN: () => checkClosesN(body),
5498
5646
  premiseCoverage: () => checkPremiseCoverage(body, changedFiles2),
5499
- issueRationale: () => checkIssueRationale(body)
5647
+ issueRationale: () => checkIssueRationale(body),
5648
+ milestoneShape: () => {
5649
+ const result = checkMilestoneShape(body);
5650
+ return { errors: result.status === "fail" ? result.errors : [] };
5651
+ }
5500
5652
  };
5501
5653
  return table[name]().errors;
5502
5654
  }
@@ -5514,7 +5666,8 @@ var BUILTIN_RECOVERY = {
5514
5666
  for: "Add a `For: <model + environment>` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
5515
5667
  closesN: "Add a `Closes #<N>` reference naming the task Issue to the body, then re-run `{cmd}`.",
5516
5668
  premiseCoverage: "Add a `Premise:` assertion whose path matches a file this change touches, then re-run `{cmd}`.",
5517
- 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}`."
5518
5671
  };
5519
5672
  function escapeRegExp(literal) {
5520
5673
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -5788,6 +5941,238 @@ function issueEditCommand(args) {
5788
5941
  runGhWrite(["issue", "edit", issueRef], ghArgs, bodyResult, json);
5789
5942
  }
5790
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
+
5791
6176
  // src/commands/new-check.ts
5792
6177
  import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
5793
6178
  import { join as join15 } from "node:path";
@@ -5832,12 +6217,205 @@ ${registration}
5832
6217
  }
5833
6218
 
5834
6219
  // src/commands/pr.ts
5835
- import { execFileSync as execFileSync11 } from "node:child_process";
5836
- 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 …";
5837
6415
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
5838
6416
  function git3(args) {
5839
6417
  try {
5840
- return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6418
+ return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5841
6419
  } catch {
5842
6420
  return "";
5843
6421
  }
@@ -5850,7 +6428,7 @@ function localChangedFiles() {
5850
6428
  return out.split(`
5851
6429
  `).map((s) => s.trim()).filter(Boolean);
5852
6430
  }
5853
- function locateBodyOrRefuse2(ghArgs, retryCommand) {
6431
+ function locateBodyOrRefuse3(ghArgs, retryCommand) {
5854
6432
  try {
5855
6433
  return locateBody(ghArgs);
5856
6434
  } catch (e) {
@@ -5871,7 +6449,7 @@ function reportPass2(json, command) {
5871
6449
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5872
6450
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5873
6451
  try {
5874
- 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"] });
5875
6453
  const url = out.trim();
5876
6454
  if (json)
5877
6455
  printJson({ validated: true, written: true, url });
@@ -5885,7 +6463,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5885
6463
  function fetchPrForgeContext(prRef) {
5886
6464
  let viewOut;
5887
6465
  try {
5888
- viewOut = execFileSync11("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
6466
+ viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5889
6467
  encoding: "utf8",
5890
6468
  stdio: ["ignore", "pipe", "pipe"]
5891
6469
  });
@@ -5910,19 +6488,25 @@ function fetchPrForgeContext(prRef) {
5910
6488
  const changedFiles2 = (parsed.files ?? []).map((f) => f.path);
5911
6489
  return { changedFiles: changedFiles2, branch: parsed.headRefName ?? "" };
5912
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
+ }
5913
6497
  function prCreateCommand(args) {
5914
6498
  const json = args.includes("--json");
5915
6499
  const validateOnly = args.includes("--validate-only");
5916
6500
  const ghArgs = args.filter((a) => a !== "--json" && a !== "--validate-only");
5917
- const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_CREATE2);
6501
+ const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_CREATE3);
5918
6502
  const body = bodyResult?.body ?? null;
5919
6503
  const title = extractTitle(ghArgs);
5920
6504
  if (body === null) {
5921
6505
  refuse([
5922
- 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}\`.`)
5923
6507
  ]);
5924
6508
  }
5925
- const sections = resolveSections("pr", RETRY_CREATE2);
6509
+ const sections = resolveSections("pr", RETRY_CREATE3);
5926
6510
  const changedFiles2 = localChangedFiles();
5927
6511
  const headCommit = git3(["rev-parse", "--verify", "--quiet", "HEAD"]);
5928
6512
  const branch = headCommit === "" ? "" : git3(["symbolic-ref", "--quiet", "--short", "HEAD"]);
@@ -5931,11 +6515,12 @@ function prCreateCommand(args) {
5931
6515
  title,
5932
6516
  sections,
5933
6517
  changedFiles: changedFiles2,
5934
- retryCommand: RETRY_CREATE2,
6518
+ retryCommand: RETRY_CREATE3,
5935
6519
  branch
5936
6520
  });
5937
6521
  if (errors.length > 0)
5938
6522
  refuse(errors);
6523
+ refuseOnBareDigits(body, RETRY_CREATE3);
5939
6524
  if (validateOnly) {
5940
6525
  reportPass2(json, "pr create");
5941
6526
  return;
@@ -5953,7 +6538,7 @@ function prEditCommand(args) {
5953
6538
  ]);
5954
6539
  }
5955
6540
  const ghArgs = rest.slice(1);
5956
- const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_EDIT2);
6541
+ const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_EDIT2);
5957
6542
  const body = bodyResult?.body ?? null;
5958
6543
  const title = extractTitle(ghArgs);
5959
6544
  if (body === null && title === null) {
@@ -5979,6 +6564,8 @@ function prEditCommand(args) {
5979
6564
  });
5980
6565
  if (errors.length > 0)
5981
6566
  refuse(errors);
6567
+ if (body !== null)
6568
+ refuseOnBareDigits(body, RETRY_EDIT2);
5982
6569
  if (validateOnly) {
5983
6570
  reportPass2(json, "pr edit");
5984
6571
  return;
@@ -5987,14 +6574,14 @@ function prEditCommand(args) {
5987
6574
  }
5988
6575
 
5989
6576
  // src/commands/pr-report.ts
5990
- import { execFileSync as execFileSync12, spawnSync as spawnSync2 } from "node:child_process";
6577
+ import { execFileSync as execFileSync13, spawnSync as spawnSync2 } from "node:child_process";
5991
6578
  import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
5992
6579
  import { join as join16 } from "node:path";
5993
6580
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
5994
6581
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
5995
6582
  function git4(args) {
5996
6583
  try {
5997
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6584
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5998
6585
  } catch {
5999
6586
  return "";
6000
6587
  }
@@ -6012,7 +6599,7 @@ class GitCommandError extends Error {
6012
6599
  }
6013
6600
  function gitStrict(args) {
6014
6601
  try {
6015
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6602
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6016
6603
  } catch (err) {
6017
6604
  const stderr = err.stderr;
6018
6605
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -6166,7 +6753,7 @@ async function prReportCommand(args) {
6166
6753
  }
6167
6754
 
6168
6755
  // src/commands/quickstart.ts
6169
- import { execFileSync as execFileSync13 } from "node:child_process";
6756
+ import { execFileSync as execFileSync14 } from "node:child_process";
6170
6757
  import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
6171
6758
  import { join as join18 } from "node:path";
6172
6759
 
@@ -6362,7 +6949,7 @@ function realDeps6() {
6362
6949
  };
6363
6950
  }
6364
6951
  function execGit(repoRoot2, args) {
6365
- 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();
6366
6953
  }
6367
6954
  function errorDetail(err) {
6368
6955
  const stderr = err.stderr;
@@ -6479,7 +7066,7 @@ async function quickstartCommand(args) {
6479
7066
  }
6480
7067
 
6481
7068
  // src/commands/review-post.ts
6482
- import { execFileSync as execFileSync14 } from "node:child_process";
7069
+ import { execFileSync as execFileSync15 } from "node:child_process";
6483
7070
  import { readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
6484
7071
  import { tmpdir as tmpdir2 } from "node:os";
6485
7072
  import { join as join19 } from "node:path";
@@ -6609,15 +7196,22 @@ function parseFlags(args) {
6609
7196
  const map = new Map;
6610
7197
  for (let i = 0;i < args.length; i++) {
6611
7198
  const a = args[i];
6612
- if (a.startsWith("--")) {
6613
- const value = args[i + 1];
6614
- if (value === undefined || value.startsWith("--")) {
6615
- map.set(a, "");
6616
- continue;
6617
- }
6618
- map.set(a, value);
6619
- i++;
7199
+ if (a === "--")
7200
+ break;
7201
+ if (!a.startsWith("--"))
7202
+ continue;
7203
+ const eq = a.indexOf("=");
7204
+ if (eq > 2) {
7205
+ map.set(a.slice(0, eq), a.slice(eq + 1));
7206
+ continue;
6620
7207
  }
7208
+ const value = args[i + 1];
7209
+ if (value === undefined || value.startsWith("--")) {
7210
+ map.set(a, "");
7211
+ continue;
7212
+ }
7213
+ map.set(a, value);
7214
+ i++;
6621
7215
  }
6622
7216
  return map;
6623
7217
  }
@@ -6639,6 +7233,11 @@ function requireTokenField(flags2, name) {
6639
7233
  return v;
6640
7234
  }
6641
7235
  function readFindingsFile(path, allowedSeverities) {
7236
+ if (path === undefined)
7237
+ return [];
7238
+ if (path.trim() === "") {
7239
+ refuseCmd("`--findings-file` was given with no path.", "Pass the path to the findings file, or omit the flag entirely if there are no findings.");
7240
+ }
6642
7241
  if (!path)
6643
7242
  return [];
6644
7243
  let content;
@@ -6674,7 +7273,7 @@ function normalizeSecurityVerdict(raw) {
6674
7273
  }
6675
7274
  function gh(args) {
6676
7275
  try {
6677
- return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
7276
+ return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6678
7277
  } catch (err) {
6679
7278
  const stderr = err.stderr;
6680
7279
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -6708,9 +7307,61 @@ function fetchComments(pr) {
6708
7307
  const parsed = JSON.parse(out);
6709
7308
  return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
6710
7309
  }
7310
+ var VALUE_FLAGS = [
7311
+ "--brief-conformance",
7312
+ "--config-scan",
7313
+ "--cost",
7314
+ "--docs",
7315
+ "--findings-file",
7316
+ "--model",
7317
+ "--pr",
7318
+ "--role",
7319
+ "--scope",
7320
+ "--secrets",
7321
+ "--secrets-evidence-file",
7322
+ "--spec-conformance",
7323
+ "--task-id",
7324
+ "--tests",
7325
+ "--tokens-in",
7326
+ "--tokens-out",
7327
+ "--verdict"
7328
+ ];
7329
+ var NULLARY_FLAGS = ["--json"];
7330
+ function unknownFlags(args, known = [...VALUE_FLAGS, ...NULLARY_FLAGS]) {
7331
+ const out = [];
7332
+ for (let i = 0;i < args.length; i++) {
7333
+ const a = args[i];
7334
+ if (known.includes(a) && !NULLARY_FLAGS.includes(a)) {
7335
+ const next = args[i + 1];
7336
+ if (next !== undefined && !next.startsWith("--"))
7337
+ i++;
7338
+ continue;
7339
+ }
7340
+ if (a === "--")
7341
+ break;
7342
+ const looksLikeFlag = a.startsWith("--") || a.startsWith("-") && a.length > 1;
7343
+ if (!looksLikeFlag)
7344
+ continue;
7345
+ const name = a.split("=")[0];
7346
+ if (!known.includes(name)) {
7347
+ out.push(name);
7348
+ continue;
7349
+ }
7350
+ if (NULLARY_FLAGS.includes(name) && a.includes("="))
7351
+ out.push(name);
7352
+ }
7353
+ return out;
7354
+ }
7355
+ function rejectUnknownFlags(args, known = [...VALUE_FLAGS, ...NULLARY_FLAGS]) {
7356
+ const unknown = unknownFlags(args, known);
7357
+ if (unknown.length === 0)
7358
+ return;
7359
+ refuseCmd(`unrecognised flag${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.`, `\`vinaya review post\` accepts: ${[...known].sort().join(", ")}. If you meant to preview without posting, note that this command has no dry-run flag — see atta-labs/vinaya#184.`);
7360
+ }
6711
7361
  async function reviewPostCommand(args) {
7362
+ rejectUnknownFlags(args);
6712
7363
  const json = args.includes("--json");
6713
- const flags2 = parseFlags(args.filter((a) => a !== "--json"));
7364
+ const flags2 = parseFlags(args.filter((a) => !NULLARY_FLAGS.includes(a)));
6714
7365
  const role = flags2.get("--role");
6715
7366
  if (role !== "code-reviewer" && role !== "security") {
6716
7367
  refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
@@ -6815,6 +7466,43 @@ function execFileForRepo(cwd) {
6815
7466
  }
6816
7467
  var PRIMARY_PORT = 3008;
6817
7468
  var FALLBACK_PORT = 3108;
7469
+ function parsePortFlag(args) {
7470
+ const found = [];
7471
+ for (let k = 0;k < args.length; k++) {
7472
+ const a = args[k];
7473
+ if (a === "--port") {
7474
+ const raw2 = args[k + 1];
7475
+ if (raw2 === undefined)
7476
+ throw new PortFlagError("`--port` requires a port number.");
7477
+ if (raw2.startsWith("-")) {
7478
+ throw new PortFlagError(`\`--port ${raw2}\` is not a port number — a port is a positive integer.`);
7479
+ }
7480
+ found.push(raw2);
7481
+ k++;
7482
+ } else if (a.startsWith("--port=")) {
7483
+ found.push(a.slice("--port=".length));
7484
+ }
7485
+ }
7486
+ if (found.length === 0)
7487
+ return null;
7488
+ if (found.length > 1 && new Set(found).size > 1) {
7489
+ throw new PortFlagError(`\`--port\` given more than once with different values (${found.join(", ")}).`);
7490
+ }
7491
+ const raw = found[0];
7492
+ if (raw === "")
7493
+ throw new PortFlagError("`--port` requires a port number.");
7494
+ if (!/^[0-9]+$/.test(raw))
7495
+ throw new PortFlagError(`\`--port ${raw}\` is not a port number.`);
7496
+ if (raw.length > 1 && raw.startsWith("0"))
7497
+ throw new PortFlagError(`\`--port ${raw}\` has a leading zero — write the port without it.`);
7498
+ const port = Number(raw);
7499
+ if (port < 1 || port > 65535)
7500
+ throw new PortFlagError(`\`--port ${raw}\` is outside the valid range 1-65535.`);
7501
+ return port;
7502
+ }
7503
+
7504
+ class PortFlagError extends Error {
7505
+ }
6818
7506
  function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
6819
7507
  let dir = cwd;
6820
7508
  for (;; ) {
@@ -6862,13 +7550,22 @@ function ensureStudioNodeModules(bundleRoot) {
6862
7550
  renameSync(packed, real);
6863
7551
  }
6864
7552
  }
6865
- async function spawnStandalone(cwd, serverPath, bundleRoot) {
7553
+ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
6866
7554
  ensureStudioNodeModules(bundleRoot);
6867
7555
  const repo = await resolveRepo(() => execFileForRepo(cwd));
6868
- const primaryFree = await isPortFree(PRIMARY_PORT);
6869
- const port = primaryFree ? PRIMARY_PORT : FALLBACK_PORT;
6870
- if (!primaryFree) {
6871
- console.info(`[studio] port ${PRIMARY_PORT} is taken — falling back to ${FALLBACK_PORT}`);
7556
+ let port;
7557
+ if (explicitPort !== null) {
7558
+ if (!await isPortFree(explicitPort)) {
7559
+ console.error(`[studio] port ${explicitPort} is taken, and \`--port\` was given explicitly refusing to bind elsewhere.`);
7560
+ return 1;
7561
+ }
7562
+ port = explicitPort;
7563
+ } else {
7564
+ const primaryFree = await isPortFree(PRIMARY_PORT);
7565
+ port = primaryFree ? PRIMARY_PORT : FALLBACK_PORT;
7566
+ if (!primaryFree) {
7567
+ console.info(`[studio] port ${PRIMARY_PORT} is taken — falling back to ${FALLBACK_PORT}`);
7568
+ }
6872
7569
  }
6873
7570
  const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
6874
7571
  if (repo)
@@ -6879,13 +7576,27 @@ async function spawnStandalone(cwd, serverPath, bundleRoot) {
6879
7576
  });
6880
7577
  }
6881
7578
  async function runStudio(cwd, args, moduleUrl = import.meta.url) {
7579
+ let explicitPort;
7580
+ try {
7581
+ explicitPort = parsePortFlag(args);
7582
+ } catch (err) {
7583
+ if (err instanceof PortFlagError) {
7584
+ console.error(`[studio] ${err.message}`);
7585
+ return 2;
7586
+ }
7587
+ throw err;
7588
+ }
6882
7589
  const target = resolveStudioTarget(cwd, moduleUrl);
6883
7590
  switch (target.kind) {
6884
7591
  case "workspace":
7592
+ if (explicitPort !== null) {
7593
+ console.error(`[studio] \`--port ${explicitPort}\` cannot apply here: this is a workspace checkout, and Studio's own dev script chooses the port. Set it there, or run against a published install.`);
7594
+ return 2;
7595
+ }
6885
7596
  return spawnDev(target.webDir, args);
6886
7597
  case "package": {
6887
7598
  const bundleRoot = join20(target.packageDir, "..", "..", "..");
6888
- return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
7599
+ return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot, explicitPort);
6889
7600
  }
6890
7601
  case "missing":
6891
7602
  console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
@@ -7000,6 +7711,10 @@ function translateHookPaths(manifest) {
7000
7711
  blocks: manifest.blocks.map((b) => b.path.startsWith(".git/hooks/") ? { ...b, path: `${TRACKED_HOOK_DIR}/${b.path.slice(".git/hooks/".length)}` } : b)
7001
7712
  };
7002
7713
  }
7714
+ function extractWorkflowTrigger(content) {
7715
+ const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
7716
+ return match?.[1] ?? null;
7717
+ }
7003
7718
  function planUpgrade(ops, repoRoot2, manifest, routing) {
7004
7719
  const entries = [];
7005
7720
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
@@ -7012,6 +7727,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
7012
7727
  const exists = existsSync19(abs2);
7013
7728
  const owned = ownedFiles.has(op.path);
7014
7729
  let action;
7730
+ let triggerChange;
7015
7731
  if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
7016
7732
  action = "keep";
7017
7733
  } else if (!owned) {
@@ -7019,13 +7735,22 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
7019
7735
  } else if (!exists) {
7020
7736
  action = "recreate";
7021
7737
  hasChanges = true;
7022
- } else if (readFileSync20(abs2, "utf-8") !== op.content) {
7023
- action = "regenerate";
7024
- hasChanges = true;
7025
7738
  } else {
7026
- action = "current";
7739
+ const diskContent = readFileSync20(abs2, "utf-8");
7740
+ if (diskContent !== op.content) {
7741
+ action = "regenerate";
7742
+ hasChanges = true;
7743
+ if (op.path.startsWith(".github/workflows/")) {
7744
+ const from = extractWorkflowTrigger(diskContent);
7745
+ const to = extractWorkflowTrigger(op.content);
7746
+ if (from && to && from !== to)
7747
+ triggerChange = { from, to };
7748
+ }
7749
+ } else {
7750
+ action = "current";
7751
+ }
7027
7752
  }
7028
- entries.push({ kind: "create-file", op, action });
7753
+ entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
7029
7754
  } else if (op.kind === "managed-block") {
7030
7755
  const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
7031
7756
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
@@ -7090,6 +7815,12 @@ function renderUpgradeDiff(plan) {
7090
7815
  case "regenerate":
7091
7816
  lines.push(` ~ regenerate ${e.op.path}`);
7092
7817
  lines.push(indent(e.op.content));
7818
+ if (e.triggerChange) {
7819
+ lines.push("");
7820
+ lines.push(` ⚠ TRIGGER CHANGE: ${e.op.path} is moving from \`${e.triggerChange.from}\` to ` + `\`${e.triggerChange.to}\`.`);
7821
+ lines.push(" GitHub evaluates a `pull_request` trigger from the pull request branch's own copy of the " + "workflow file, but a `pull_request_target` trigger from the copy on the BASE branch — never " + "the pull request branch's. The pull request that carries this exact change matches NEITHER: " + "as `pull_request` it no longer matches (the pull request branch now declares " + "`pull_request_target`), and as `pull_request_target` the base branch still declares the old " + "trigger until this pull request merges. No workflow run fires under either event, so this " + "file's required check-run never appears for this pull request — not failing, not pending, " + "simply absent — and a ruleset or branch-protection rule that requires it will block this pull " + "request from merging through the normal flow.");
7822
+ lines.push(" To get this pull request merged: temporarily add a ruleset bypass actor (or, under classic " + "branch protection, a temporary admin override) for the merge, then remove it once this pull " + "request is in.");
7823
+ }
7093
7824
  break;
7094
7825
  case "recreate":
7095
7826
  lines.push(` + recreate ${e.op.path} (recorded as owned but missing on disk)`);
@@ -7254,12 +7985,12 @@ async function upgradeCommand(args) {
7254
7985
  }
7255
7986
 
7256
7987
  // src/commands/waiver.ts
7257
- import { execFileSync as execFileSync15 } from "node:child_process";
7988
+ import { execFileSync as execFileSync16 } from "node:child_process";
7258
7989
  function labelFor(kind) {
7259
7990
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
7260
7991
  }
7261
7992
  function gh2(args) {
7262
- 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();
7263
7994
  }
7264
7995
  function detectPrNumber() {
7265
7996
  try {
@@ -7269,15 +8000,15 @@ function detectPrNumber() {
7269
8000
  return null;
7270
8001
  }
7271
8002
  }
7272
- var VALUE_FLAGS = ["--reason"];
8003
+ var VALUE_FLAGS2 = ["--reason"];
7273
8004
  var BOOLEAN_FLAGS = ["--print-only"];
7274
8005
  function positionalArgs(args) {
7275
8006
  const result = [];
7276
8007
  for (let i = 0;i < args.length; i++) {
7277
8008
  const a = args[i];
7278
- if (VALUE_FLAGS.some((f) => a.startsWith(`${f}=`)))
8009
+ if (VALUE_FLAGS2.some((f) => a.startsWith(`${f}=`)))
7279
8010
  continue;
7280
- if (VALUE_FLAGS.includes(a)) {
8011
+ if (VALUE_FLAGS2.includes(a)) {
7281
8012
  i += 1;
7282
8013
  continue;
7283
8014
  }
@@ -7359,8 +8090,8 @@ Reason: ${reason}`];
7359
8090
  }
7360
8091
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
7361
8092
  `);
7362
- execFileSync15("gh", editArgs, { stdio: "inherit", env: process.env });
7363
- 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 });
7364
8095
  process.stdout.write(`
7365
8096
  ✓ Label applied and reason posted as a PR comment.
7366
8097
  `);
@@ -7397,7 +8128,7 @@ var COMMANDS = [
7397
8128
  },
7398
8129
  {
7399
8130
  name: "init product",
7400
- description: "Register a project in .vinaya/projects.md (and its project:<name> label) in an already-initialized repo",
8131
+ description: "Register a project in .vinaya/projects.md in an already-initialized repo",
7401
8132
  flags: [
7402
8133
  {
7403
8134
  flag: "--path <path>",
@@ -7406,8 +8137,8 @@ var COMMANDS = [
7406
8137
  ],
7407
8138
  details: [
7408
8139
  "Writes (or appends to) `.vinaya/projects.md` — the registry Vinaya Studio's tranche board resolves a project's board link against. Idempotent: re-running with the same name updates nothing.",
7409
- "Also creates a `project:<name>` label (create-if-absent) informational only, not read by any shipped check or by Studio.",
7410
- "A missing GitHub remote skips the label (the one forge-reaching op) but still writes the registry row, which is a pure local file write."
8140
+ "Reaches no forge and needs no GitHub remote or credentials: the registry row is a pure local file write, and it is deliberately NOT recorded in the ownership manifest, so `eject` does not reverse adopter-declared data.",
8141
+ "Creates no label. Project is a field, not a label: a task Issue declares its project in the body's `**Project:**` field, and a `project:*` label is ignored wherever one still exists."
7411
8142
  ],
7412
8143
  status: "shipped"
7413
8144
  },
@@ -7520,6 +8251,37 @@ var COMMANDS = [
7520
8251
  ],
7521
8252
  status: "shipped"
7522
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
+ },
7523
8285
  {
7524
8286
  name: "review post",
7525
8287
  description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
@@ -7662,6 +8424,12 @@ var COMMANDS = [
7662
8424
  {
7663
8425
  name: "studio",
7664
8426
  description: "Launch Vinaya Studio — runs the Studio dev app when its source (apps/vinaya-studio/web) is in a checkout above the current directory; a published install launches its bundled standalone server instead",
8427
+ flags: [
8428
+ {
8429
+ flag: "--port <n>",
8430
+ description: "Bind this exact port. Without the flag the default is unchanged — 3008, falling back to 3108 when it is taken. With it there is no fallback: a taken port is refused, so you always know which server answered. Accepts `--port 3208` and `--port=3208`. Applies to a published install; in a workspace checkout Studio's own dev script owns the port and the flag is refused."
8431
+ }
8432
+ ],
7665
8433
  details: [
7666
8434
  "Resolution happens in this order: a workspace checkout carrying `apps/vinaya-studio/web` (Studio's source, which lives in the attalabs monorepo — not this repository) runs the dev app directly; a published install's `studio-standalone/` bundle (fetched from attalabs' release artifact at publish time, see `scripts/bundle-studio.ts`) runs that bundled server; anything else — a publish that shipped without the bundle — gets an explicit refusal and exit 1 rather than a silent no-op.",
7667
8435
  "Every published `@attalabs/vinaya` build ships the `studio-standalone/` bundle: `prepack` fetches attalabs’ latest CI-built standalone Studio artifact and assembles it into the tarball before publish."
@@ -7812,6 +8580,18 @@ try {
7812
8580
  }
7813
8581
  break;
7814
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
+ }
7815
8595
  case "review": {
7816
8596
  const [subcommand, ...rest] = args;
7817
8597
  if (subcommand === "post") {