@attalabs/vinaya 0.17.1 → 0.18.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 (68) hide show
  1. package/README.md +35 -1
  2. package/dist/checks/bin/check-body-bare-digits.js +2 -1
  3. package/dist/checks/bin/check-coherence.js +2 -1
  4. package/dist/checks/bin/check-dispatch-readiness.js +2 -1
  5. package/dist/checks/bin/check-doc-coverage-push.js +2 -1
  6. package/dist/checks/bin/check-doc-coverage.js +2 -1
  7. package/dist/checks/bin/check-first-push-dispatch.js +2 -1
  8. package/dist/checks/bin/check-review-gate.js +2 -1
  9. package/dist/index.js +226 -72
  10. package/package.json +1 -1
  11. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  12. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  13. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  14. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  15. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  16. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  17. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  18. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  20. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  23. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  24. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  25. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  26. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  27. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  28. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  29. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  30. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  31. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  32. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  33. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  35. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  36. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  37. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  46. 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
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1ccgndq._.js → [root-of-the-server]__0z47hfl._.js} +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  63. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{40x7f5hm7a5tk.js → 2myi4phnshkfw.js} +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0l_vubs1ephe5.js → 3z3a3gb6he23i.js} +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ojfbn3yvy-8_.js → 415k4k1rf3swg.js} +1 -1
  66. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → 8tXGbArNoU7_MriIVYvN0}/_buildManifest.js +0 -0
  67. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → 8tXGbArNoU7_MriIVYvN0}/_clientMiddlewareManifest.js +0 -0
  68. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → 8tXGbArNoU7_MriIVYvN0}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -20,7 +20,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
20
20
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
21
21
  | `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
22
22
  | `vinaya review post --role code-reviewer \| security --pr <n> ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (verdict, findings, per-field text) instead of a hand-typed comment. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`Judged head:` line from validated inputs, never from caller-supplied text; refuses a contradictory verdict (a BLOCKER/CRITICAL-or-HIGH finding with a clean verdict) before posting anything; and after posting, re-fetches the comment and refuses to exit 0 unless it re-parses through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls. `--json` for the enveloped machine form. |
23
- | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. |
23
+ | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. `--port <n>` binds an exact port — see below. |
24
24
 
25
25
  ## Config
26
26
 
@@ -74,6 +74,8 @@ One thing git cannot version is the config itself: **each fresh clone runs `git
74
74
 
75
75
  Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory (husky's `prepare` script owns per-clone wiring), and a repo with its own active raw hooks in `.git/hooks` stays on the legacy append-a-managed-block layout there — re-routing `core.hooksPath` would silently disable the adopter's own hooks. On that legacy layout `vinaya doctor` warns that clones have no hooks, and `vinaya upgrade` migrates to the tracked layout as soon as nothing foreign would be disabled.
76
76
 
77
+ On that legacy `.git/hooks` layout, note where the hook actually lives: hooks are never per-worktree, so its real home is the main checkout's shared hooks directory (`git rev-parse --git-common-dir`), which from a linked worktree is outside that worktree's own root. `vinaya eject` follows the hook there and bounds the removal to that directory's `hooks/` subtree, rather than to the worktree root — so ejecting from a linked worktree strips the hook instead of leaving it armed. A canonically spelled block path resolving anywhere else is refused, and the refusal is whole-run: `eject` removes nothing at all rather than making a partial destructive pass. (The `.git/` test is byte-exact, so a non-canonical spelling takes the working-tree branch instead — nothing vinaya generates produces one.)
78
+
77
79
  ## Agent skills
78
80
 
79
81
  The emitter that will generate skill pointers under `.agents/skills/vinaya-<role>/SKILL.md`, for tools natively scanning `.agents/skills/` (Codex, Antigravity, Grok Build), is built — `vinaya init` does not call it yet. Each file, once wired, is a 3-line pointer delegating to `vinaya doctrine --role <role>` at read time. User-facing flag, `init`/`upgrade`/`eject`/`doctor` wiring, and the `--agents` selection flag land in #152.
@@ -136,6 +138,38 @@ The generated review-authority workflows run only default-branch code: the requi
136
138
 
137
139
  `.vinaya/doc-owners` binds code globs to the docs that must change with them; `vinaya check`'s C5 gate enforces that binding against each pull request's own diff — it fires only when a changed file matches a bound glob. That leaves a gap C5 cannot close on its own: a binding whose code was deleted or renamed wholesale matches nothing on any later diff, ever again, and reads as healthy forever. `vinaya doctor` closes it separately — it never fails `vinaya check` — by walking every binding against the repo's full tracked-file list and reporting any whose code glob matches zero tracked files anywhere in the repo, or whose in-repo doc pointer doesn't exist on disk. Report-only, like every other `vinaya doctor` diagnostic: it repoints or removes nothing itself.
138
140
 
141
+ ## Brief-schema divergence
142
+
143
+ `briefSchema` in `vinaya.config.json` is yours: `vinaya upgrade` preserves it wholesale and never rewrites it. On its own that ownership has a silent cost — nothing else reads it either, so a builtin deleted to work around a defect stays deleted, with no later upgrade to repair it and nothing to surface it.
144
+
145
+ `vinaya doctor` reports the divergence. It compares your `briefSchema.pr` and `briefSchema.issue` against the set `vinaya init` ships and names any builtin that is absent — nothing more. Extra sections, whether a second builtin or your own `heading`/`field`/`phrase` matcher, are additions rather than weakenings and are never reported. The finding is `info` severity and can never fail your CI: running without a builtin is legitimate configuration, and the goal is to make the choice visible, not to argue you back to the default.
146
+
147
+ Once an omission is deliberate, name it in `briefSchema.ack` and it goes quiet:
148
+
149
+ ```json
150
+ { "briefSchema": { "ack": ["closesN"] } }
151
+ ```
152
+
153
+ `ack` grants nothing and gates nothing — it only silences this report, and acking a builtin you still declare changes no behaviour anywhere. A deletion you did not intend, having no ack, keeps surfacing.
154
+
155
+ Two things worth knowing about its shape. `ack` is one flat list rather than per-kind, so acking a name that the shipped default declares for **both** kinds — `project` is the only such name today — silences it for both. And a config carrying no `briefSchema` key at all diverges on every builtin, so it reports on every run until you either declare the sections or ack them; that is the one shape where this becomes standing output rather than a one-time notice.
156
+
157
+ Report-only, like every other `vinaya doctor` diagnostic: it restores nothing itself.
158
+
159
+ ## Pinning Studio's port
160
+
161
+ `vinaya studio` binds `3008`, or `3108` when that is taken. `--port <n>` overrides both, and the override does **not** fall back: if the port you named is busy, the command refuses instead of quietly binding a different one.
162
+
163
+ That asymmetry is deliberate. The default pair exists so a casual `vinaya studio` still comes up when something else holds `3008`. But naming a port is how you buy certainty about which server answered you — and silently moving to another port spends exactly that. Run two Studio servers on one machine without it and a `200` from `/studio` tells you nothing about which process replied.
164
+
165
+ ```bash
166
+ vinaya studio --port 3208 # or --port=3208
167
+ ```
168
+
169
+ A malformed value exits `2` before anything starts: no value, a non-number, one outside `1`–`65535`, a leading zero, or the flag given twice with different values.
170
+
171
+ The flag applies to a published install, where this CLI launches the bundled server and owns the port. In a checkout carrying Studio's source it is refused, because that path runs Studio's own dev script, which picks its own port and ignores what it is passed — accepting the flag there would report a port nothing ever binds.
172
+
139
173
  ## Known limits
140
174
 
141
175
  The five core AEG checks (`coherence`, `dispatch-readiness`, and siblings) are bound to the Vinaya development repository — they read governance documents relative to it. Outside a Vinaya workspace, `vinaya check --all` reports those checks as `status: 'error'` rather than crashing.
@@ -3184,7 +3184,8 @@ var BriefSectionSchema = z.union([
3184
3184
  ]);
3185
3185
  var BriefSchemaSchema = z.object({
3186
3186
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3187
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
3187
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3188
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
3188
3189
  });
3189
3190
  function isSafeRepoRelPath(p) {
3190
3191
  if (p.length === 0)
@@ -3207,7 +3207,8 @@ var BriefSectionSchema = z2.union([
3207
3207
  ]);
3208
3208
  var BriefSchemaSchema = z2.object({
3209
3209
  pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3210
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional()
3210
+ issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3211
+ ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
3211
3212
  });
3212
3213
  function isSafeRepoRelPath(p) {
3213
3214
  if (p.length === 0)
@@ -3205,7 +3205,8 @@ var BriefSectionSchema = z2.union([
3205
3205
  ]);
3206
3206
  var BriefSchemaSchema = z2.object({
3207
3207
  pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3208
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional()
3208
+ issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3209
+ ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
3209
3210
  });
3210
3211
  function isSafeRepoRelPath(p) {
3211
3212
  if (p.length === 0)
@@ -3206,7 +3206,8 @@ var BriefSectionSchema = z.union([
3206
3206
  ]);
3207
3207
  var BriefSchemaSchema = z.object({
3208
3208
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3209
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
3209
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3210
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
3210
3211
  });
3211
3212
  function isSafeRepoRelPath(p) {
3212
3213
  if (p.length === 0)
@@ -3206,7 +3206,8 @@ var BriefSectionSchema = z.union([
3206
3206
  ]);
3207
3207
  var BriefSchemaSchema = z.object({
3208
3208
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3209
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
3209
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3210
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
3210
3211
  });
3211
3212
  function isSafeRepoRelPath(p) {
3212
3213
  if (p.length === 0)
@@ -3204,7 +3204,8 @@ var BriefSectionSchema = z2.union([
3204
3204
  ]);
3205
3205
  var BriefSchemaSchema = z2.object({
3206
3206
  pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3207
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional()
3207
+ issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
3208
+ ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
3208
3209
  });
3209
3210
  function isSafeRepoRelPath(p) {
3210
3211
  if (p.length === 0)
@@ -3190,7 +3190,8 @@ var BriefSectionSchema = z.union([
3190
3190
  ]);
3191
3191
  var BriefSchemaSchema = z.object({
3192
3192
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3193
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
3193
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3194
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
3194
3195
  });
3195
3196
  function isSafeRepoRelPath(p) {
3196
3197
  if (p.length === 0)
package/dist/index.js CHANGED
@@ -2025,18 +2025,6 @@ function buildInitOps(ctx) {
2025
2025
  ops.push({ kind: "print", message: CODEOWNERS_NOTE, group: "Branch protection (printed, never applied)" });
2026
2026
  return ops;
2027
2027
  }
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
2028
 
2041
2029
  // src/lib/ops.ts
2042
2030
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -2139,7 +2127,8 @@ var BriefSectionSchema = z.union([
2139
2127
  ]);
2140
2128
  var BriefSchemaSchema = z.object({
2141
2129
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2142
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
2130
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
2131
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
2143
2132
  });
2144
2133
  var MANAGED_MANIFEST_VERSION = 2;
2145
2134
  function isSafeRepoRelPath(p) {
@@ -2343,17 +2332,22 @@ ${end}`;
2343
2332
  function abs(repoRoot, relPath) {
2344
2333
  return join4(repoRoot, relPath);
2345
2334
  }
2335
+ var GIT_DIR_PREFIX = ".git/";
2346
2336
  function resolveManagedBlockPath(repoRoot, opPath) {
2347
- if (!opPath.startsWith(".git/"))
2337
+ if (!opPath.startsWith(GIT_DIR_PREFIX))
2348
2338
  return join4(repoRoot, opPath);
2339
+ return join4(gitCommonDir(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
2340
+ }
2341
+ function gitCommonDir(repoRoot) {
2349
2342
  try {
2350
2343
  const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
2351
2344
  encoding: "utf8"
2352
2345
  }).trim();
2353
- const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
2354
- return join4(gitDir, opPath.slice(".git/".length));
2346
+ if (commonDir === "")
2347
+ return join4(repoRoot, ".git");
2348
+ return isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
2355
2349
  } catch {
2356
- return join4(repoRoot, opPath);
2350
+ return join4(repoRoot, ".git");
2357
2351
  }
2358
2352
  }
2359
2353
  function containedAbs(repoRoot, relPath) {
@@ -2363,6 +2357,16 @@ function containedAbs(repoRoot, relPath) {
2363
2357
  return null;
2364
2358
  return target === root || target.startsWith(root + sep) ? target : null;
2365
2359
  }
2360
+ function containedManagedBlockAbs(repoRoot, relPath) {
2361
+ if (!relPath.startsWith(GIT_DIR_PREFIX))
2362
+ return containedAbs(repoRoot, relPath);
2363
+ const commonDir = gitCommonDir(repoRoot);
2364
+ const hooksRoot = resolve(join4(commonDir, "hooks"));
2365
+ const target = resolve(join4(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
2366
+ if (target === hooksRoot)
2367
+ return null;
2368
+ return target.startsWith(hooksRoot + sep) ? target : null;
2369
+ }
2366
2370
  function fileContains(repoRoot, relPath, needle) {
2367
2371
  const p = resolveManagedBlockPath(repoRoot, relPath);
2368
2372
  if (!existsSync3(p))
@@ -2583,7 +2587,7 @@ function planEject(manifest, repoRoot) {
2583
2587
  const actions = [];
2584
2588
  const escapes = [];
2585
2589
  for (const b of manifest.blocks) {
2586
- const p = containedAbs(repoRoot, b.path);
2590
+ const p = containedManagedBlockAbs(repoRoot, b.path);
2587
2591
  if (p === null) {
2588
2592
  escapes.push(b.path);
2589
2593
  continue;
@@ -2648,7 +2652,7 @@ function applyEject(plan, repoRoot) {
2648
2652
  if (a.kind === "strip-block") {
2649
2653
  if (!a.present)
2650
2654
  continue;
2651
- const p = containedAbs(repoRoot, a.path);
2655
+ const p = containedManagedBlockAbs(repoRoot, a.path);
2652
2656
  if (p === null)
2653
2657
  continue;
2654
2658
  const content = readFileSync3(p, "utf-8");
@@ -4735,6 +4739,31 @@ function diagnoseBlastRadiusDeprecation(repoRoot, config) {
4735
4739
  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
4740
  ];
4737
4741
  }
4742
+ var BRIEF_KIND_LABEL = {
4743
+ pr: "PR bodies",
4744
+ issue: "task Issue bodies"
4745
+ };
4746
+ function builtinsIn(sections) {
4747
+ return (sections ?? []).flatMap((s) => ("builtin" in s) ? [s.builtin] : []);
4748
+ }
4749
+ function diagnoseBriefSchemaDrift(config) {
4750
+ if (!config)
4751
+ return [];
4752
+ const shipped = starterConfig().briefSchema;
4753
+ const acked = new Set(config.briefSchema?.ack ?? []);
4754
+ const findings = [];
4755
+ for (const kind of ["pr", "issue"]) {
4756
+ const expected = builtinsIn(shipped?.[kind]?.sections);
4757
+ if (expected.length === 0)
4758
+ continue;
4759
+ const present = new Set(builtinsIn(config.briefSchema?.[kind]?.sections));
4760
+ const missing = expected.filter((b) => !present.has(b) && !acked.has(b));
4761
+ if (missing.length === 0)
4762
+ continue;
4763
+ 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."));
4764
+ }
4765
+ return findings;
4766
+ }
4738
4767
  function diagnoseEnvDeclarations(repoRoot, config) {
4739
4768
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
4740
4769
  const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
@@ -4893,6 +4922,7 @@ async function runDoctor(args, deps) {
4893
4922
  findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
4894
4923
  }
4895
4924
  findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4925
+ findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
4896
4926
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4897
4927
  findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
4898
4928
  findings.push(...diagnoseGlobalConfigChecks());
@@ -5034,7 +5064,7 @@ async function runEject(args, deps) {
5034
5064
  const plan = planEject(read.manifest, repo.repoRoot);
5035
5065
  const unarm = read.manifest.blocks.some((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)) && await deps.readHooksPath(repo.repoRoot) === TRACKED_HOOK_DIR;
5036
5066
  if (plan.escapes.length > 0) {
5037
- console.error("Error: the ownership manifest records paths that resolve OUTSIDE this repo:");
5067
+ console.error("Error: the ownership manifest records paths that resolve OUTSIDE the bounds vinaya may touch:");
5038
5068
  for (const p of plan.escapes)
5039
5069
  console.error(` ${p}`);
5040
5070
  console.error("Refusing to remove anything. This manifest is corrupt or hand-edited — fix vinaya.config.json, then re-run.");
@@ -5315,17 +5345,9 @@ async function runInitProduct(args, deps) {
5315
5345
  console.error("Error: this repo is not Vinaya-initialized yet. Run `vinaya init` first.");
5316
5346
  return 1;
5317
5347
  }
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
5348
  const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
5325
5349
  process.stdout.write(`vinaya init product ${name} — the full diff:
5326
5350
 
5327
- `);
5328
- process.stdout.write(`${renderInstallDiff(plan)}
5329
5351
  `);
5330
5352
  process.stdout.write(`── Project registry ─────────────────────────────
5331
5353
  `);
@@ -5346,29 +5368,11 @@ async function runInitProduct(args, deps) {
5346
5368
  }
5347
5369
  }
5348
5370
  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
5371
  process.stdout.write(`
5358
5372
  Governed product area '${name}' scaffolded.
5359
5373
  `);
5360
5374
  return 0;
5361
5375
  }
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
5376
  async function initCommand(args) {
5373
5377
  process.exit(await runInit(args, realDeps5()));
5374
5378
  }
@@ -6609,15 +6613,22 @@ function parseFlags(args) {
6609
6613
  const map = new Map;
6610
6614
  for (let i = 0;i < args.length; i++) {
6611
6615
  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++;
6616
+ if (a === "--")
6617
+ break;
6618
+ if (!a.startsWith("--"))
6619
+ continue;
6620
+ const eq = a.indexOf("=");
6621
+ if (eq > 2) {
6622
+ map.set(a.slice(0, eq), a.slice(eq + 1));
6623
+ continue;
6624
+ }
6625
+ const value = args[i + 1];
6626
+ if (value === undefined || value.startsWith("--")) {
6627
+ map.set(a, "");
6628
+ continue;
6620
6629
  }
6630
+ map.set(a, value);
6631
+ i++;
6621
6632
  }
6622
6633
  return map;
6623
6634
  }
@@ -6639,6 +6650,11 @@ function requireTokenField(flags2, name) {
6639
6650
  return v;
6640
6651
  }
6641
6652
  function readFindingsFile(path, allowedSeverities) {
6653
+ if (path === undefined)
6654
+ return [];
6655
+ if (path.trim() === "") {
6656
+ 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.");
6657
+ }
6642
6658
  if (!path)
6643
6659
  return [];
6644
6660
  let content;
@@ -6708,9 +6724,61 @@ function fetchComments(pr) {
6708
6724
  const parsed = JSON.parse(out);
6709
6725
  return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
6710
6726
  }
6727
+ var VALUE_FLAGS = [
6728
+ "--brief-conformance",
6729
+ "--config-scan",
6730
+ "--cost",
6731
+ "--docs",
6732
+ "--findings-file",
6733
+ "--model",
6734
+ "--pr",
6735
+ "--role",
6736
+ "--scope",
6737
+ "--secrets",
6738
+ "--secrets-evidence-file",
6739
+ "--spec-conformance",
6740
+ "--task-id",
6741
+ "--tests",
6742
+ "--tokens-in",
6743
+ "--tokens-out",
6744
+ "--verdict"
6745
+ ];
6746
+ var NULLARY_FLAGS = ["--json"];
6747
+ function unknownFlags(args, known = [...VALUE_FLAGS, ...NULLARY_FLAGS]) {
6748
+ const out = [];
6749
+ for (let i = 0;i < args.length; i++) {
6750
+ const a = args[i];
6751
+ if (known.includes(a) && !NULLARY_FLAGS.includes(a)) {
6752
+ const next = args[i + 1];
6753
+ if (next !== undefined && !next.startsWith("--"))
6754
+ i++;
6755
+ continue;
6756
+ }
6757
+ if (a === "--")
6758
+ break;
6759
+ const looksLikeFlag = a.startsWith("--") || a.startsWith("-") && a.length > 1;
6760
+ if (!looksLikeFlag)
6761
+ continue;
6762
+ const name = a.split("=")[0];
6763
+ if (!known.includes(name)) {
6764
+ out.push(name);
6765
+ continue;
6766
+ }
6767
+ if (NULLARY_FLAGS.includes(name) && a.includes("="))
6768
+ out.push(name);
6769
+ }
6770
+ return out;
6771
+ }
6772
+ function rejectUnknownFlags(args, known = [...VALUE_FLAGS, ...NULLARY_FLAGS]) {
6773
+ const unknown = unknownFlags(args, known);
6774
+ if (unknown.length === 0)
6775
+ return;
6776
+ 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.`);
6777
+ }
6711
6778
  async function reviewPostCommand(args) {
6779
+ rejectUnknownFlags(args);
6712
6780
  const json = args.includes("--json");
6713
- const flags2 = parseFlags(args.filter((a) => a !== "--json"));
6781
+ const flags2 = parseFlags(args.filter((a) => !NULLARY_FLAGS.includes(a)));
6714
6782
  const role = flags2.get("--role");
6715
6783
  if (role !== "code-reviewer" && role !== "security") {
6716
6784
  refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
@@ -6815,6 +6883,43 @@ function execFileForRepo(cwd) {
6815
6883
  }
6816
6884
  var PRIMARY_PORT = 3008;
6817
6885
  var FALLBACK_PORT = 3108;
6886
+ function parsePortFlag(args) {
6887
+ const found = [];
6888
+ for (let k = 0;k < args.length; k++) {
6889
+ const a = args[k];
6890
+ if (a === "--port") {
6891
+ const raw2 = args[k + 1];
6892
+ if (raw2 === undefined)
6893
+ throw new PortFlagError("`--port` requires a port number.");
6894
+ if (raw2.startsWith("-")) {
6895
+ throw new PortFlagError(`\`--port ${raw2}\` is not a port number — a port is a positive integer.`);
6896
+ }
6897
+ found.push(raw2);
6898
+ k++;
6899
+ } else if (a.startsWith("--port=")) {
6900
+ found.push(a.slice("--port=".length));
6901
+ }
6902
+ }
6903
+ if (found.length === 0)
6904
+ return null;
6905
+ if (found.length > 1 && new Set(found).size > 1) {
6906
+ throw new PortFlagError(`\`--port\` given more than once with different values (${found.join(", ")}).`);
6907
+ }
6908
+ const raw = found[0];
6909
+ if (raw === "")
6910
+ throw new PortFlagError("`--port` requires a port number.");
6911
+ if (!/^[0-9]+$/.test(raw))
6912
+ throw new PortFlagError(`\`--port ${raw}\` is not a port number.`);
6913
+ if (raw.length > 1 && raw.startsWith("0"))
6914
+ throw new PortFlagError(`\`--port ${raw}\` has a leading zero — write the port without it.`);
6915
+ const port = Number(raw);
6916
+ if (port < 1 || port > 65535)
6917
+ throw new PortFlagError(`\`--port ${raw}\` is outside the valid range 1-65535.`);
6918
+ return port;
6919
+ }
6920
+
6921
+ class PortFlagError extends Error {
6922
+ }
6818
6923
  function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
6819
6924
  let dir = cwd;
6820
6925
  for (;; ) {
@@ -6862,13 +6967,22 @@ function ensureStudioNodeModules(bundleRoot) {
6862
6967
  renameSync(packed, real);
6863
6968
  }
6864
6969
  }
6865
- async function spawnStandalone(cwd, serverPath, bundleRoot) {
6970
+ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
6866
6971
  ensureStudioNodeModules(bundleRoot);
6867
6972
  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}`);
6973
+ let port;
6974
+ if (explicitPort !== null) {
6975
+ if (!await isPortFree(explicitPort)) {
6976
+ console.error(`[studio] port ${explicitPort} is taken, and \`--port\` was given explicitly refusing to bind elsewhere.`);
6977
+ return 1;
6978
+ }
6979
+ port = explicitPort;
6980
+ } else {
6981
+ const primaryFree = await isPortFree(PRIMARY_PORT);
6982
+ port = primaryFree ? PRIMARY_PORT : FALLBACK_PORT;
6983
+ if (!primaryFree) {
6984
+ console.info(`[studio] port ${PRIMARY_PORT} is taken — falling back to ${FALLBACK_PORT}`);
6985
+ }
6872
6986
  }
6873
6987
  const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
6874
6988
  if (repo)
@@ -6879,13 +6993,27 @@ async function spawnStandalone(cwd, serverPath, bundleRoot) {
6879
6993
  });
6880
6994
  }
6881
6995
  async function runStudio(cwd, args, moduleUrl = import.meta.url) {
6996
+ let explicitPort;
6997
+ try {
6998
+ explicitPort = parsePortFlag(args);
6999
+ } catch (err) {
7000
+ if (err instanceof PortFlagError) {
7001
+ console.error(`[studio] ${err.message}`);
7002
+ return 2;
7003
+ }
7004
+ throw err;
7005
+ }
6882
7006
  const target = resolveStudioTarget(cwd, moduleUrl);
6883
7007
  switch (target.kind) {
6884
7008
  case "workspace":
7009
+ if (explicitPort !== null) {
7010
+ 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.`);
7011
+ return 2;
7012
+ }
6885
7013
  return spawnDev(target.webDir, args);
6886
7014
  case "package": {
6887
7015
  const bundleRoot = join20(target.packageDir, "..", "..", "..");
6888
- return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
7016
+ return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot, explicitPort);
6889
7017
  }
6890
7018
  case "missing":
6891
7019
  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 +7128,10 @@ function translateHookPaths(manifest) {
7000
7128
  blocks: manifest.blocks.map((b) => b.path.startsWith(".git/hooks/") ? { ...b, path: `${TRACKED_HOOK_DIR}/${b.path.slice(".git/hooks/".length)}` } : b)
7001
7129
  };
7002
7130
  }
7131
+ function extractWorkflowTrigger(content) {
7132
+ const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
7133
+ return match?.[1] ?? null;
7134
+ }
7003
7135
  function planUpgrade(ops, repoRoot2, manifest, routing) {
7004
7136
  const entries = [];
7005
7137
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
@@ -7012,6 +7144,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
7012
7144
  const exists = existsSync19(abs2);
7013
7145
  const owned = ownedFiles.has(op.path);
7014
7146
  let action;
7147
+ let triggerChange;
7015
7148
  if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
7016
7149
  action = "keep";
7017
7150
  } else if (!owned) {
@@ -7019,13 +7152,22 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
7019
7152
  } else if (!exists) {
7020
7153
  action = "recreate";
7021
7154
  hasChanges = true;
7022
- } else if (readFileSync20(abs2, "utf-8") !== op.content) {
7023
- action = "regenerate";
7024
- hasChanges = true;
7025
7155
  } else {
7026
- action = "current";
7156
+ const diskContent = readFileSync20(abs2, "utf-8");
7157
+ if (diskContent !== op.content) {
7158
+ action = "regenerate";
7159
+ hasChanges = true;
7160
+ if (op.path.startsWith(".github/workflows/")) {
7161
+ const from = extractWorkflowTrigger(diskContent);
7162
+ const to = extractWorkflowTrigger(op.content);
7163
+ if (from && to && from !== to)
7164
+ triggerChange = { from, to };
7165
+ }
7166
+ } else {
7167
+ action = "current";
7168
+ }
7027
7169
  }
7028
- entries.push({ kind: "create-file", op, action });
7170
+ entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
7029
7171
  } else if (op.kind === "managed-block") {
7030
7172
  const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
7031
7173
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
@@ -7090,6 +7232,12 @@ function renderUpgradeDiff(plan) {
7090
7232
  case "regenerate":
7091
7233
  lines.push(` ~ regenerate ${e.op.path}`);
7092
7234
  lines.push(indent(e.op.content));
7235
+ if (e.triggerChange) {
7236
+ lines.push("");
7237
+ lines.push(` ⚠ TRIGGER CHANGE: ${e.op.path} is moving from \`${e.triggerChange.from}\` to ` + `\`${e.triggerChange.to}\`.`);
7238
+ 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.");
7239
+ 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.");
7240
+ }
7093
7241
  break;
7094
7242
  case "recreate":
7095
7243
  lines.push(` + recreate ${e.op.path} (recorded as owned but missing on disk)`);
@@ -7269,15 +7417,15 @@ function detectPrNumber() {
7269
7417
  return null;
7270
7418
  }
7271
7419
  }
7272
- var VALUE_FLAGS = ["--reason"];
7420
+ var VALUE_FLAGS2 = ["--reason"];
7273
7421
  var BOOLEAN_FLAGS = ["--print-only"];
7274
7422
  function positionalArgs(args) {
7275
7423
  const result = [];
7276
7424
  for (let i = 0;i < args.length; i++) {
7277
7425
  const a = args[i];
7278
- if (VALUE_FLAGS.some((f) => a.startsWith(`${f}=`)))
7426
+ if (VALUE_FLAGS2.some((f) => a.startsWith(`${f}=`)))
7279
7427
  continue;
7280
- if (VALUE_FLAGS.includes(a)) {
7428
+ if (VALUE_FLAGS2.includes(a)) {
7281
7429
  i += 1;
7282
7430
  continue;
7283
7431
  }
@@ -7397,7 +7545,7 @@ var COMMANDS = [
7397
7545
  },
7398
7546
  {
7399
7547
  name: "init product",
7400
- description: "Register a project in .vinaya/projects.md (and its project:<name> label) in an already-initialized repo",
7548
+ description: "Register a project in .vinaya/projects.md in an already-initialized repo",
7401
7549
  flags: [
7402
7550
  {
7403
7551
  flag: "--path <path>",
@@ -7406,8 +7554,8 @@ var COMMANDS = [
7406
7554
  ],
7407
7555
  details: [
7408
7556
  "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."
7557
+ "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.",
7558
+ "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
7559
  ],
7412
7560
  status: "shipped"
7413
7561
  },
@@ -7662,6 +7810,12 @@ var COMMANDS = [
7662
7810
  {
7663
7811
  name: "studio",
7664
7812
  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",
7813
+ flags: [
7814
+ {
7815
+ flag: "--port <n>",
7816
+ 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."
7817
+ }
7818
+ ],
7665
7819
  details: [
7666
7820
  "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
7821
  "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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attalabs/vinaya",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Vinaya — Agentic Engineering Harness. Deterministic checks every AI coding agent must satisfy before merge.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1 +1 @@
1
- p5sWGp4ANwORiaX4Kk6U2
1
+ 8tXGbArNoU7_MriIVYvN0