@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.
- package/README.md +37 -1
- package/aeg-root/contracts/architect-planner.md +72 -0
- package/aeg-root/roles/architect.md +91 -0
- package/aeg-root/roles/planner.md +14 -15
- package/aeg-root/tranche-model.md +9 -1
- package/dist/checks/bin/check-body-bare-digits.js +62 -14
- package/dist/checks/bin/check-branch-topology.js +57 -12
- package/dist/checks/bin/check-brief-shape.js +57 -12
- package/dist/checks/bin/check-closes-n.js +57 -12
- package/dist/checks/bin/check-coherence.js +62 -14
- package/dist/checks/bin/check-dead-branch-push.js +57 -12
- package/dist/checks/bin/check-dispatch-readiness.js +129 -41
- package/dist/checks/bin/check-doc-coverage-push.js +62 -14
- package/dist/checks/bin/check-doc-coverage.js +62 -14
- package/dist/checks/bin/check-evidence-fresh.js +57 -12
- package/dist/checks/bin/check-first-push-dispatch.js +130 -42
- package/dist/checks/bin/check-issue-assignment.js +57 -12
- package/dist/checks/bin/check-no-disk-state.js +57 -12
- package/dist/checks/bin/check-reader-resolvable-prose.js +57 -12
- package/dist/checks/bin/check-registry-gates.js +57 -12
- package/dist/checks/bin/check-review-gate.js +62 -14
- package/dist/checks/bin/check-single-plan-pr.js +57 -12
- package/dist/checks/bin/check-test-plan.js +57 -12
- package/dist/index.js +889 -109
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
- 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
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- 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
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -5
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{40x7f5hm7a5tk.js → 2r36w820aqwb9.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ojfbn3yvy-8_.js → 2xoclb4g7d9n7.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0l_vubs1ephe5.js → 3ocvtlhgckh81.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_clientMiddlewareManifest.js +0 -0
- /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(
|
|
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
|
-
|
|
2354
|
-
|
|
2466
|
+
if (commonDir === "")
|
|
2467
|
+
return join4(repoRoot, ".git");
|
|
2468
|
+
return isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
|
|
2355
2469
|
} catch {
|
|
2356
|
-
return join4(repoRoot,
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
"
|
|
3065
|
-
|
|
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
|
-
|
|
3068
|
-
|
|
3069
|
-
`
|
|
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
|
|
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
|
|
5836
|
-
|
|
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
|
+
""": '"',
|
|
6376
|
+
"'": "'",
|
|
6377
|
+
"<": "<",
|
|
6378
|
+
">": ">",
|
|
6379
|
+
"&": "&"
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 \`${
|
|
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",
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
map.set(a,
|
|
6619
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
8009
|
+
if (VALUE_FLAGS2.some((f) => a.startsWith(`${f}=`)))
|
|
7279
8010
|
continue;
|
|
7280
|
-
if (
|
|
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
|
-
|
|
7363
|
-
|
|
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
|
|
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
|
-
"
|
|
7410
|
-
"
|
|
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") {
|