@attalabs/vinaya 0.23.0 → 0.24.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 (117) hide show
  1. package/README.md +1 -0
  2. package/dist/checks/bin/check-body-bare-digits.js +7 -1
  3. package/dist/checks/bin/check-branch-topology.js +27 -1
  4. package/dist/checks/bin/check-brief-shape.js +7 -1
  5. package/dist/checks/bin/check-changeset-coverage.js +7 -1
  6. package/dist/checks/bin/check-closes-n.js +27 -1
  7. package/dist/checks/bin/check-coherence.js +27 -1
  8. package/dist/checks/bin/check-dead-branch-push.js +7 -1
  9. package/dist/checks/bin/check-dispatch-readiness.js +27 -1
  10. package/dist/checks/bin/check-doc-coverage-push.js +7 -1
  11. package/dist/checks/bin/check-doc-coverage.js +7 -1
  12. package/dist/checks/bin/check-doctrine-no-procedures.js +7 -1
  13. package/dist/checks/bin/check-doctrine-portability.js +7 -1
  14. package/dist/checks/bin/check-evidence-fresh.js +7 -1
  15. package/dist/checks/bin/check-exec-bits.js +7 -1
  16. package/dist/checks/bin/check-first-push-dispatch.js +27 -1
  17. package/dist/checks/bin/check-issue-assignment.js +27 -1
  18. package/dist/checks/bin/check-main-branch-refusal.js +13 -2
  19. package/dist/checks/bin/check-no-disk-state.js +7 -1
  20. package/dist/checks/bin/check-pr-report-density.js +7 -1
  21. package/dist/checks/bin/check-quoted-command.js +38 -12
  22. package/dist/checks/bin/check-reader-resolvable-prose.js +38 -12
  23. package/dist/checks/bin/check-registry-gates.js +7 -1
  24. package/dist/checks/bin/check-retired-vocabulary.js +38 -12
  25. package/dist/checks/bin/check-review-gate.js +7 -1
  26. package/dist/checks/bin/check-single-plan-pr.js +7 -1
  27. package/dist/checks/bin/check-test-plan.js +7 -1
  28. package/dist/checks/bin/check-token-collection-wired.js +7 -1
  29. package/dist/checks/bin/check-token-report.js +7 -1
  30. package/dist/checks/bin/check-workspace-escape.js +7 -1
  31. package/dist/index.js +362 -87
  32. package/package.json +1 -1
  33. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +9 -4
  34. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +7 -1
  35. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +462 -0
  36. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +405 -4
  37. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +19 -0
  38. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
  39. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +44 -0
  40. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
  41. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +128 -5
  42. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +3 -0
  43. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +25 -3
  44. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +17 -5
  45. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
  46. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +35 -0
  47. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +3 -3
  48. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +116 -7
  49. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
  50. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +25 -12
  51. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +70 -11
  52. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  54. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  91. 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
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1mcitst._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +3 -3
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  110. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0jer_drkl1e4r.js → 0zebsmmk1bvnb.js} +4 -4
  111. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{20x-t0a0v129z.js → 1uogpj5w2n5ju.js} +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ou_81ahqw31e.js → 3m1kgax7j2vgs.js} +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  114. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  115. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
  116. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
  117. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { type AnchorField, anchoredRegion, stripCode } from './anchored-region'
16
- import { parsePremiseBlock } from './premise-check'
16
+ import { parsePremiseBlock, premiseBlockText } from './premise-check'
17
17
  import { locateTestPlanSection } from './test-plan-section'
18
18
 
19
19
  export type BriefSectionResult = { status: 'pass' | 'fail'; errors: string[] }
@@ -126,14 +126,37 @@ function testPlanRegion(prBody: string): string {
126
126
  * judge whether tags are correctly assigned or whether `unit-tests-only` is
127
127
  * justified by the surface map.
128
128
  */
129
+ /**
130
+ * Presence-only Test Plan detector — three acceptable shapes since task 12
131
+ * (#387) rendered the `[agent]` half of a Test Plan as a fenced command list
132
+ * rather than a checkbox: the `unit-tests-only` sentinel; a
133
+ * `**[agent]**`/`**[principal]**`-tagged checkbox line (the pre-#387 shape,
134
+ * still valid on a PR below `AGENT_BOXES_REFUSED_SINCE_PR` and for
135
+ * `[principal]` items on every PR); or a fenced code block anywhere in the
136
+ * Test Plan region (the fenced-command-list shape). This gate does not judge
137
+ * WHICH shape a given PR number must use — `checkNoAgentBoxes`, below, is the
138
+ * rule that refuses a checkbox `[agent]` item on a PR at or above the rollout
139
+ * constant; this one only asks "is there a Test Plan at all."
140
+ */
129
141
  export function checkTestPlan(prBody: string): BriefSectionResult {
130
- const region = testPlanRegion(prBody)
142
+ const located = locateTestPlanSection(prBody)
143
+ const region = located.found ? located.section : prBody
131
144
  if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region)) return { status: 'pass', errors: [] }
132
145
  if (/\*\*\[(?:agent|principal)\]\*\*/.test(region)) return { status: 'pass', errors: [] }
146
+ // Fenced-block detection only counts when the section was genuinely
147
+ // LOCATED, never on the whole-body fallback: `testPlanRegion`'s fallback
148
+ // exists so the sentinel/tag checks above still work on a body with no
149
+ // recognizable heading, but a fence can appear anywhere in a brief (Step
150
+ // 0's own worktree command, a Part's own command block) — trusting ANY
151
+ // fence in the whole body would pass a brief whose Test Plan section was
152
+ // deleted outright, as long as it kept some unrelated fence elsewhere
153
+ // (found live, fixing this same rule: `checkBriefSections`'s own "missing
154
+ // Test Plan" regression fixture stopped failing until this guard landed).
155
+ if (located.found && extractFencedBlocks(region).length > 0) return { status: 'pass', errors: [] }
133
156
  return {
134
157
  status: 'fail',
135
158
  errors: [
136
- 'brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item.'
159
+ 'brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, a fenced `[agent]` command list, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item.'
137
160
  ]
138
161
  }
139
162
  }
@@ -466,6 +489,370 @@ export function inferBranchFromBody(prBody: string): string {
466
489
  return m?.[1] ?? ''
467
490
  }
468
491
 
492
+ /**
493
+ * The shell-command-word vocabulary this task's two prose-shape rules share
494
+ * (`checkCommandsCarryOutput` and `doctrine-no-procedures.ts`'s sweep) — one
495
+ * list, so the two rules can never silently diverge on what counts as "a
496
+ * command". Exported for that reuse, not for callers to extend at runtime.
497
+ */
498
+ export const COMMAND_WORDS = ['export', 'bun', 'gh', 'git', 'grep', 'sed', 'cat', 'diff', 'vinaya'] as const
499
+
500
+ /**
501
+ * Rollout PR number for the four brief-shape rules this task added
502
+ * (`checkNoUnpinnedCodeClaims`, `checkCommandsCarryOutput`,
503
+ * `checkConsumerTests`, `checkDefeatCases`). A PR numbered below this is
504
+ * grandfathered: the CI shim (`check-brief-shape.ts`) reports a finding from
505
+ * one of these four rules as informational, never a failure. A PR at or
506
+ * above it is held to the rule for real. `verify-brief.ts` (authoring time,
507
+ * pre-dispatch) has no PR number and applies all four unconditionally —
508
+ * grandfathering is a CI-only rollout concern, not a grammar relaxation
509
+ * (round-2 ruling addendum 1).
510
+ */
511
+ export const BRIEF_RULES_SINCE_PR = 394
512
+
513
+ /**
514
+ * Rollout PR number for `checkNoAgentBoxes`, below — a second, distinct
515
+ * cutover from `BRIEF_RULES_SINCE_PR` above, per the Principal's ruling
516
+ * (2026-09-03, after PR #395): an agent never ticks a box or edits a PR
517
+ * body, so the `[agent]` half of a Test Plan stops being checkboxes and
518
+ * becomes a fenced command list (task 12, #387). A PR numbered below this
519
+ * is grandfathered — its checkbox `[agent]` items were written before the
520
+ * ruling and are reported informationally, never a failure. `verify-brief.ts`
521
+ * (authoring time, pre-dispatch) has no PR number and applies the rule
522
+ * unconditionally, same grandfathering-is-CI-only posture as
523
+ * `BRIEF_RULES_SINCE_PR`.
524
+ */
525
+ export const AGENT_BOXES_REFUSED_SINCE_PR = 396
526
+
527
+ const AGENT_BOX_LINE_RE = /^-\s*\[[ xX]\]\s*\*{2}\[agent\]\*{2}/im
528
+
529
+ /**
530
+ * Refuses a Test Plan whose `[agent]` half is still a checkbox item — the
531
+ * shape the fenced-command-list rule (task 12, #387) replaces. Presence-only
532
+ * within the Test Plan region (`testPlanRegion`), like every sibling check
533
+ * in this file: whether the fenced list a body carries instead is any GOOD
534
+ * is a Reviewer/Verification judgment, not this gate's.
535
+ */
536
+ export function checkNoAgentBoxes(prBody: string): BriefSectionResult {
537
+ const region = testPlanRegion(prBody)
538
+ if (!AGENT_BOX_LINE_RE.test(region)) return { status: 'pass', errors: [] }
539
+ return {
540
+ status: 'fail',
541
+ errors: [
542
+ 'brief-validation no agent boxes: the Test Plan carries a checkbox `[agent]` item — an agent never ticks a box or edits a PR body (Principal ruling, 2026-09-03, after PR #395). Render the `[agent]` half as a fenced command list instead; `vinaya pr report` runs each command from the PR head and writes its actual output into the AEG:EVIDENCE block. `[principal]` checkboxes are unaffected.'
543
+ ]
544
+ }
545
+ }
546
+
547
+ /**
548
+ * True for an error message produced by one of the five grandfatherable
549
+ * rules below (the original four, plus `checkNoAgentBoxes`) — classified by
550
+ * each rule's own distinct message prefix, since `checkBriefSections`
551
+ * aggregates every sub-check's errors into one flat `string[]` and this is
552
+ * the only reader that ever needs to tell them apart from the rest.
553
+ */
554
+ export function isGrandfatherableBriefRuleError(message: string): boolean {
555
+ return rolloutThresholdFor(message) !== null
556
+ }
557
+
558
+ /**
559
+ * The rollout constant a given error message is grandfathered against, or
560
+ * `null` for a message this rollout scheme does not cover (always blocking,
561
+ * on every PR). Two thresholds exist today (`BRIEF_RULES_SINCE_PR`,
562
+ * `AGENT_BOXES_REFUSED_SINCE_PR`) — a message is matched to whichever rule
563
+ * produced it, never a single global cutover, so a later third threshold
564
+ * can be added here without touching either existing one's grandfather
565
+ * window.
566
+ */
567
+ function rolloutThresholdFor(message: string): number | null {
568
+ if (
569
+ message.startsWith('brief-validation unpinned code claim:') ||
570
+ message.startsWith('brief-validation commands carry output:') ||
571
+ message.startsWith('brief-validation consumer tests:') ||
572
+ message.startsWith('brief-validation defeat cases:')
573
+ ) {
574
+ return BRIEF_RULES_SINCE_PR
575
+ }
576
+ if (message.startsWith('brief-validation no agent boxes:')) return AGENT_BOXES_REFUSED_SINCE_PR
577
+ return null
578
+ }
579
+
580
+ /**
581
+ * Splits `checkBriefSections`'s flat error list into `blocking` (fails the
582
+ * check) and `info` (printed, never a failure) — the CI shim
583
+ * (`check-brief-shape.ts`) is the only caller, but the split is a pure
584
+ * function of `(errors, prNumber)` so it is unit-testable directly.
585
+ *
586
+ * `prNumber === null` (no `PR_NUMBER`, or an unparseable one) is NOT
587
+ * grandfathered — only a real, parsed number below a message's own
588
+ * threshold (`rolloutThresholdFor`) is. Fail-closed: a check that can't tell
589
+ * which PR it's grading must not quietly waive rules it has no number to
590
+ * check against.
591
+ */
592
+ export function partitionBriefErrorsByRollout(
593
+ errors: string[],
594
+ prNumber: number | null
595
+ ): { blocking: string[]; info: string[] } {
596
+ const blocking: string[] = []
597
+ const info: string[] = []
598
+ for (const e of errors) {
599
+ const threshold = rolloutThresholdFor(e)
600
+ const grandfathered = threshold !== null && prNumber !== null && prNumber < threshold
601
+ ;(grandfathered ? info : blocking).push(e)
602
+ }
603
+ return { blocking, info }
604
+ }
605
+
606
+ /**
607
+ * One fenced block's raw span, content, and language tag — `start`/`end` are
608
+ * char offsets into the original text. `lang` is the fence's info-string,
609
+ * lowercased and trimmed to its first word (` ```ts ` → `'ts'`, ` ``` ` →
610
+ * `''`) — `doctrine-no-procedures.ts` uses it to exempt a non-shell block
611
+ * (e.g. a TypeScript illustration whose two `export` lines are not a
612
+ * command sequence, task 10 round-2 ruling item 2).
613
+ */
614
+ export type FencedBlock = { start: number; end: number; lang: string; content: string }
615
+
616
+ /**
617
+ * Every fenced (``` or ~~~) code block in `text`, in document order, with its
618
+ * raw content, language tag, and char-offset span. Tolerant of the 3-space
619
+ * list-item indentation every brief's own numbered steps use (` \`\`\``) —
620
+ * the fence marker need not sit at column 0. Not a full CommonMark
621
+ * implementation (no nested-fence-length edge cases beyond "the closer
622
+ * repeats the opener's exact run"), which this repo's own doctrine/brief
623
+ * prose never exercises.
624
+ *
625
+ * Shared by `checkCommandsCarryOutput` (below) and `doctrine-no-procedures.ts`
626
+ * — one fence scanner, never a second copy of this pattern.
627
+ */
628
+ export function extractFencedBlocks(text: string): FencedBlock[] {
629
+ const re = /^[ \t]*(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^[ \t]*\1[ \t]*$/gm
630
+ const blocks: FencedBlock[] = []
631
+ let m: RegExpExecArray | null = re.exec(text)
632
+ while (m !== null) {
633
+ const lang = (m[2] as string).trim().toLowerCase().split(/\s+/)[0] ?? ''
634
+ blocks.push({ start: m.index, end: m.index + m[0].length, lang, content: m[3] as string })
635
+ m = re.exec(text)
636
+ }
637
+ return blocks
638
+ }
639
+
640
+ function firstNonBlankLine(text: string): string {
641
+ const line = text.split('\n').find((l) => l.trim().length > 0)
642
+ return line?.trim() ?? ''
643
+ }
644
+
645
+ /**
646
+ * A file-and-line reference — `<path>.<ext>:<digits>` — the shape a brief
647
+ * states a code fact by pointer instead of by an executed command or a
648
+ * `Premise:` pin. Extension list is this repo's own doctrine/code file kinds;
649
+ * deliberately not "any word after a dot" (that would also catch a semver
650
+ * string or a decimal figure with a trailing count).
651
+ */
652
+ const FILE_LINE_RE = /\b[\w./-]+\.(?:tsx?|jsx?|mjs|cjs|md|mdx|json|ya?ml|sh|py|toml):\d+\b/g
653
+
654
+ /**
655
+ * Rule (i) (task 10, Issue #385) — a brief states no code fact as a bare
656
+ * `file:line` prose pointer; it either pins the fact in `Premise:` (re-
657
+ * asserted at dispatch, `verify-dispatch --premise`) or shows the executed
658
+ * command whose output names the line. A pointer that survives outside both
659
+ * homes is a claim nobody re-checks — exactly the class task 9's rule
660
+ * forbids in prose (PR #382's `security-archivist.md:88` mention, backticked
661
+ * but never pinned nor shown as command output).
662
+ *
663
+ * Scans `stripCode(…, { inlineSpans: 'keep' })`: fenced/indented **blocks**
664
+ * are removed (a worked example's own line numbers are not a live claim),
665
+ * but a single-backtick inline span is left as literal text — the PR #382
666
+ * sentence was exactly an inline span, and this rule must still catch it.
667
+ * This is why `maskCode` (which blanks inline spans too) is the wrong tool
668
+ * here, unlike `checkClosesN`/`isBriefShaped` elsewhere in this file.
669
+ */
670
+ export function checkNoUnpinnedCodeClaims(prBody: string): BriefSectionResult {
671
+ const premiseText = premiseBlockText(prBody)
672
+ const withoutPremise = premiseText ? prBody.replace(premiseText, '') : prBody
673
+ const scanned = stripCode(withoutPremise, { inlineSpans: 'keep' })
674
+ const matches = scanned.match(FILE_LINE_RE) ?? []
675
+ if (matches.length === 0) return { status: 'pass', errors: [] }
676
+ return {
677
+ status: 'fail',
678
+ errors: matches.map(
679
+ (m) =>
680
+ `brief-validation unpinned code claim: "${m}" states a code fact by file-and-line reference outside a \`Premise:\` pin and outside a fenced code block — pin it in \`Premise:\`, or show the executed command and its output instead.`
681
+ )
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Section `num`'s own text (the heading line's content, ending at the next
687
+ * heading of any level 1-4) — `§4`/`§5`/`§6` share this numbered-heading
688
+ * shape with `headingCheck`'s keyword form, so this mirrors that pattern
689
+ * rather than inventing a second heading grammar. `null` when no such
690
+ * section exists (the composer's other checks already judge whether a
691
+ * required section is missing; these two rules apply only when it is
692
+ * present).
693
+ */
694
+ function extractNumberedSection(prBody: string, num: number): string | null {
695
+ const startRe = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?${num}[a-z]?\\.\\s`, 'im')
696
+ const start = startRe.exec(prBody)
697
+ if (!start) return null
698
+ const afterStart = start.index + start[0].length
699
+ const rest = prBody.slice(afterStart)
700
+ const next = /^#{1,4}\s/m.exec(rest)
701
+ return next ? rest.slice(0, next.index) : rest
702
+ }
703
+
704
+ function commandWordOf(line: string): string {
705
+ return line.trim().split(/\s+/)[0] ?? ''
706
+ }
707
+
708
+ function isCommandBlock(content: string): boolean {
709
+ return (COMMAND_WORDS as readonly string[]).includes(commandWordOf(firstNonBlankLine(content)))
710
+ }
711
+
712
+ /** The Step 0 exemption — `git worktree add …` is a command with no separate output block by convention. */
713
+ function isStep0Block(content: string): boolean {
714
+ return firstNonBlankLine(content).startsWith('git worktree add')
715
+ }
716
+
717
+ /**
718
+ * Rule (ii) (task 10, Issue #385; tightened by the round-2 ruling item 1) —
719
+ * in a brief's `§5` (Pre-flight checks) or `§6` (Numbered parts), a fenced
720
+ * block that opens with a shell command must be immediately followed by
721
+ * another fenced block that does NOT itself open with a shell command — its
722
+ * output. The Step `0` `git worktree add` block is exempt: it is a setup
723
+ * command with no output to show, by the convention every brief's own
724
+ * pre-flight step already follows.
725
+ *
726
+ * **The immediately-following fence, never merely "a later one".** The first
727
+ * cut of this rule accepted `blocks[i + 1]` existing at all, which let three
728
+ * undocumented command blocks in a row pass as long as a single trailing
729
+ * output fence sat after the last of them — each of the first two command
730
+ * blocks' own "next fence" was itself another un-followed command, and the
731
+ * rule never noticed (round-2 ruling, found live reviewing this PR's own
732
+ * diff). A command block's output claim is only satisfied by the very next
733
+ * fence, and only if that fence is not itself a command.
734
+ *
735
+ * Deliberately loose about what sits *between* the two fences (the prose
736
+ * separating them, never counted) — this brief's own pre-flight steps
737
+ * (`§5` items 4-9) interleave a sentence of prose between a command fence
738
+ * and its output fence ("Output the Brief Author obtained at authoring
739
+ * time…"), and the rule must not fail the brief that documents it.
740
+ */
741
+ export function checkCommandsCarryOutput(prBody: string): BriefSectionResult {
742
+ const errors: string[] = []
743
+ for (const [label, num] of [
744
+ ['5', 5],
745
+ ['6', 6]
746
+ ] as const) {
747
+ const section = extractNumberedSection(prBody, num)
748
+ if (!section) continue
749
+ const blocks = extractFencedBlocks(section)
750
+ for (let i = 0; i < blocks.length; i++) {
751
+ const block = blocks[i] as FencedBlock
752
+ if (isStep0Block(block.content) || !isCommandBlock(block.content)) continue
753
+ const next = blocks[i + 1]
754
+ if (!next || isCommandBlock(next.content)) {
755
+ errors.push(
756
+ `brief-validation commands carry output: §${label} has a command block ("${firstNonBlankLine(block.content)}") not immediately followed by a non-command output block — the very next fenced block must hold that command's actual output, not another command.`
757
+ )
758
+ }
759
+ }
760
+ }
761
+ return errors.length === 0 ? { status: 'pass', errors: [] } : { status: 'fail', errors }
762
+ }
763
+
764
+ const CONSUMER_TESTS_SENTINEL_RE = /consumer-tests\s*:\s*none\s*[-—–]\s*\S/i
765
+
766
+ /** Every distinct `packages/<pkg>/` reference in `text` — the packages a §4 surface map names. */
767
+ function packagesNamedIn(text: string): string[] {
768
+ const re = /\bpackages\/([A-Za-z0-9_-]+)\//g
769
+ const pkgs = new Set<string>()
770
+ let m: RegExpExecArray | null = re.exec(text)
771
+ while (m !== null) {
772
+ pkgs.add(m[1] as string)
773
+ m = re.exec(text)
774
+ }
775
+ return [...pkgs]
776
+ }
777
+
778
+ /** Whether `text` names a test-shaped path (`*.test.<ext>`) under workspace directory `consumerDir` (e.g. `apps/cli`, `packages/sources`). */
779
+ function hasTestPathForConsumer(text: string, consumerDir: string): boolean {
780
+ const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
781
+ const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`)
782
+ return re.test(text)
783
+ }
784
+
785
+ /**
786
+ * Rule (iii) (task 10, Issue #385) — a brief whose `§4` names a path under a
787
+ * shared `packages/<pkg>/` must also name, for every workspace package that
788
+ * depends on `@attalabs/<pkg>`, a test path proving that consumer still
789
+ * works, or the sentinel `consumer-tests: none — <reason>` opting out with a
790
+ * stated reason. A shared-package edit with no consumer awareness at all is
791
+ * exactly the gap task 9's rule targets alongside the unpinned-claim and
792
+ * missing-output shapes.
793
+ *
794
+ * `consumersOf` is injected (never `fs`/`package.json` reads here) — the CLI
795
+ * shim (`check-brief-shape.ts`) builds it once from the real workspace
796
+ * dependency graph. Both the trigger and its satisfying test path/sentinel
797
+ * are read from `§4` alone, never the whole body — scanning the whole body
798
+ * let a brief's own §2 prose, quoting the sentinel grammar as an example of
799
+ * the rule (exactly this task's own dispatched brief), silently satisfy the
800
+ * rule it was merely describing. `§4` is where a surface map states its
801
+ * blast radius; that is also where this rule's own answer belongs.
802
+ */
803
+ export function checkConsumerTests(prBody: string, consumersOf: (pkg: string) => string[]): BriefSectionResult {
804
+ const section4 = extractNumberedSection(prBody, 4)
805
+ if (!section4) return { status: 'pass', errors: [] }
806
+ if (CONSUMER_TESTS_SENTINEL_RE.test(section4)) return { status: 'pass', errors: [] }
807
+
808
+ const errors: string[] = []
809
+ for (const pkg of packagesNamedIn(section4)) {
810
+ for (const consumerDir of consumersOf(pkg)) {
811
+ if (hasTestPathForConsumer(section4, consumerDir)) continue
812
+ errors.push(
813
+ `brief-validation consumer tests: §4 names a path under packages/${pkg}/, and ${consumerDir} depends on @attalabs/${pkg}, but no test path under ${consumerDir} is named in §4 — name one, or add \`consumer-tests: none — <reason>\`.`
814
+ )
815
+ }
816
+ }
817
+ return errors.length === 0 ? { status: 'pass', errors: [] } : { status: 'fail', errors }
818
+ }
819
+
820
+ /** A check bin filename (`check-<slug>.ts`) — the shape both a §4 "Create"/"Modify" entry and a registry entry use for a check's own name. */
821
+ const CHECK_NAME_RE = /\bcheck-[a-z0-9-]+(?:\.ts)?\b/
822
+
823
+ /** A command that writes to the forge — as opposed to a read-only query — the class this rule wants a `Defeat cases:` line for. */
824
+ const FORGE_WRITE_COMMAND_RE =
825
+ /\bgh\s+(?:pr|issue)\s+(?:create|merge|close|comment|edit|review)\b|\bgit\s+push\b|\bvinaya\s+pr\s+(?:create|report\s+--write)\b/i
826
+
827
+ const DEFEAT_CASES_RE = /defeat cases\s*:/i
828
+
829
+ /**
830
+ * Rule (v) — Principal ruling amending Issue #385: a brief whose `§4`
831
+ * names a check (a `check-<slug>.ts` bin, or its registry entry) or a
832
+ * forge-writing command (`gh pr create`, `git push`, `vinaya pr create`, …)
833
+ * must carry a `Defeat cases:` line in `§6` — the inputs that would defeat
834
+ * the check, or the ones a forge-writing command must not accidentally
835
+ * trigger on. A check or a write path shipped with no stated defeat case is
836
+ * exactly the kind of untested edge this task's own sibling rules exist to
837
+ * close for a brief's prose claims; this one closes it for the check/command
838
+ * itself.
839
+ */
840
+ export function checkDefeatCases(prBody: string): BriefSectionResult {
841
+ const section4 = extractNumberedSection(prBody, 4)
842
+ if (!section4) return { status: 'pass', errors: [] }
843
+ if (!CHECK_NAME_RE.test(section4) && !FORGE_WRITE_COMMAND_RE.test(section4)) return { status: 'pass', errors: [] }
844
+
845
+ const section6 = extractNumberedSection(prBody, 6) ?? ''
846
+ if (DEFEAT_CASES_RE.test(section6)) return { status: 'pass', errors: [] }
847
+
848
+ return {
849
+ status: 'fail',
850
+ errors: [
851
+ 'brief-validation defeat cases: §4 names a check or a forge-writing command, but §6 carries no `Defeat cases:` line — name the inputs that would defeat this check, or that this command must not accidentally trigger on.'
852
+ ]
853
+ }
854
+ }
855
+
469
856
  /** Composition knobs for `checkBriefSections` — see each field. */
470
857
  export type BriefSectionsOptions = {
471
858
  /**
@@ -480,6 +867,15 @@ export type BriefSectionsOptions = {
480
867
  * linkage is a task-branch obligation; brief completeness is not.
481
868
  */
482
869
  requireClosesN?: boolean
870
+ /**
871
+ * Consumer enumeration for `checkConsumerTests` (task 10) — workspace
872
+ * directories (e.g. `apps/cli`) whose `package.json` depends on
873
+ * `@attalabs/<pkg>`. Defaults to `() => []`, which makes rule (iii) a
874
+ * no-op — the callers that don't wire a real dependency graph (tests,
875
+ * `verify-brief.ts`) keep today's behavior rather than silently failing
876
+ * on an empty consumer list.
877
+ */
878
+ consumersOf?: (pkg: string) => string[]
483
879
  }
484
880
 
485
881
  /**
@@ -492,7 +888,7 @@ export function checkBriefSections(
492
888
  readTier: (body: string) => 0 | 1 | 3 | null,
493
889
  options: BriefSectionsOptions = {}
494
890
  ): { errors: string[] } {
495
- const { requireClosesN = true } = options
891
+ const { requireClosesN = true, consumersOf = () => [] } = options
496
892
  const results = [
497
893
  checkTierField(prBody, readTier),
498
894
  checkTestPlan(prBody),
@@ -505,6 +901,11 @@ export function checkBriefSections(
505
901
  checkAutonomyClause(prBody),
506
902
  checkProjectField(prBody),
507
903
  checkForField(prBody),
904
+ checkNoUnpinnedCodeClaims(prBody),
905
+ checkNoAgentBoxes(prBody),
906
+ checkCommandsCarryOutput(prBody),
907
+ checkConsumerTests(prBody, consumersOf),
908
+ checkDefeatCases(prBody),
508
909
  ...(requireClosesN ? [checkClosesN(prBody)] : [])
509
910
  ]
510
911
  return { errors: results.flatMap((r) => r.errors) }
@@ -379,6 +379,25 @@ export function checkD1(
379
379
  const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry)
380
380
  if (!depEntry) continue // unknown dep — not a D1 concern
381
381
 
382
+ // A self-dependency is unsatisfiable by construction, so it is never a
383
+ // real D1 state — it is proof of a parser defect upstream. Reported as
384
+ // an INTERNAL error rather than as "is not closed", which reads as a
385
+ // legitimate unmet dependency and invites routing around the gate.
386
+ // Mirrors `isSelfDependency` in `dispatch-gate.ts`; deliberately a
387
+ // second small local predicate rather than a shared export, since the
388
+ // two modules match on different fact shapes.
389
+ const sameTask = depEntry.trancheSlug === e.trancheSlug && depEntry.task.id === e.task.id
390
+ const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue
391
+ if (sameTask || sameIssue) {
392
+ failures.push({
393
+ issue: e.task.issue,
394
+ tranche: e.trancheSlug,
395
+ task: e.task.id,
396
+ reason: `INTERNAL: parsed a self-dependency (depends-on ${dep}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream.`
397
+ })
398
+ continue
399
+ }
400
+
382
401
  const depFacts = depEntry.facts
383
402
  const depClosed = depFacts?.issueState === 'closed'
384
403
  if (!depClosed) {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `checkConsumerTests`'s consumer enumeration (`brief-validation.ts`, task 10,
3
+ * Issue #385; corrected by the round-2 ruling item 3) — every workspace
4
+ * member whose own `package.json` depends on `@attalabs/<pkg>`, for "every
5
+ * workspace package depending on `@attalabs/<pkg>`" to mean what it says.
6
+ *
7
+ * The first cut reused `deriveWorkspacePackageDomains` (`blast-radius-domains.ts`),
8
+ * which deliberately collapses to `packages/*` members only — correct for
9
+ * that module's own collision-domain purpose, wrong here: it silently
10
+ * exempted `apps/cli`, a real `@attalabs/aeg-core` consumer, from a rule
11
+ * that promises "every workspace package" (found live reviewing this PR's
12
+ * own diff). This module resolves every workspace glob instead —
13
+ * `apps/*` and `packages/*` alike — via `resolveWorkspaceEntry`, the same
14
+ * glob grammar `deriveWorkspacePackageDomains` uses, reused rather than
15
+ * re-derived a second time.
16
+ *
17
+ * Zero I/O: `listDirs`/`readManifest` are injected by each of the two real
18
+ * entry points — `packages/aeg-core/bin/verify-brief.ts` (authoring-time,
19
+ * pre-dispatch) and `apps/cli`'s `check-brief-shape.ts` (CI) — so both share
20
+ * ONE enumeration rather than two independently-maintained copies that can
21
+ * silently diverge (round-2 ruling item 4).
22
+ */
23
+
24
+ import { resolveWorkspaceEntry } from './blast-radius-domains'
25
+
26
+ export type PackageManifest = { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
27
+
28
+ /**
29
+ * Every workspace member directory (`apps/cli`, `packages/aeg-core`, …),
30
+ * resolved from `workspaces` (positive entries minus any `!`-negated ones,
31
+ * same semantics as `deriveWorkspacePackageDomains`) — but NOT collapsed to
32
+ * `packages/*` only, since a real consumer can live anywhere in the
33
+ * workspace.
34
+ */
35
+ export function deriveWorkspaceMemberDirs(workspaces: string[], listDirs: (dir: string) => string[]): string[] {
36
+ const positive = workspaces.filter((entry) => !entry.startsWith('!'))
37
+ const negative = workspaces.filter((entry) => entry.startsWith('!')).map((entry) => entry.slice(1))
38
+
39
+ const excluded = new Set<string>()
40
+ for (const entry of negative) {
41
+ for (const member of resolveWorkspaceEntry(entry, listDirs)) excluded.add(member)
42
+ }
43
+
44
+ const members = new Set<string>()
45
+ for (const entry of positive) {
46
+ for (const member of resolveWorkspaceEntry(entry, listDirs)) {
47
+ if (!excluded.has(member)) members.add(member)
48
+ }
49
+ }
50
+ return [...members].sort()
51
+ }
52
+
53
+ /**
54
+ * Builds `checkConsumerTests`'s `consumersOf` callback: for a package `pkg`
55
+ * (as named in a brief's `§4`, e.g. `packages/aeg-core/` → `'aeg-core'`),
56
+ * every workspace member directory (self excluded) whose own manifest lists
57
+ * `@attalabs/<pkg>` in `dependencies` or `devDependencies`.
58
+ */
59
+ export function buildConsumersOf(
60
+ workspaces: string[],
61
+ listDirs: (dir: string) => string[],
62
+ readManifest: (dir: string) => PackageManifest | null
63
+ ): (pkg: string) => string[] {
64
+ const members = deriveWorkspaceMemberDirs(workspaces, listDirs)
65
+
66
+ return (pkg: string): string[] => {
67
+ const target = `@attalabs/${pkg}`
68
+ return members.filter((dir) => {
69
+ if (dir === `packages/${pkg}`) return false
70
+ const manifest = readManifest(dir)
71
+ if (!manifest) return false
72
+ return target in (manifest.dependencies ?? {}) || target in (manifest.devDependencies ?? {})
73
+ })
74
+ }
75
+ }
@@ -145,6 +145,39 @@ export type DispatchGateInput = {
145
145
 
146
146
  export type DispatchResult = { ready: boolean; blockers: string[] }
147
147
 
148
+ /**
149
+ * True when a parsed `depends-on` edge points back at its own host task.
150
+ *
151
+ * A self-dependency is unsatisfiable by construction, so it is never a real
152
+ * gate state — its presence is proof of a defect in the edge text or in its
153
+ * resolution. `parseRationaleDeps` (`@attalabs/aeg-forge-state`) reads only
154
+ * a labeled `Depends-on:`/`Conflicts-with:` field's own comma-separated,
155
+ * id-shaped tokens; since Issue #347 an unlabeled/bare span elsewhere in the
156
+ * "Dependency rationale" section is never read as an edge, whatever its
157
+ * shape (`parse-rationale-deps.ts`'s own module comment states the rule). A
158
+ * slug-qualified token in that list (`aeg-governance-hardening #368`) still
159
+ * resolves its trailing bare number against the NAMED tranche, not the host
160
+ * — a resolver bug there is what would land a task depending on itself, on
161
+ * that tranche's own task `1`.
162
+ *
163
+ * Reported as an INTERNAL error rather than through the ordinary
164
+ * "not merged yet" branch below, because that message reads as a legitimate
165
+ * serialization and invites the reader to route around it: two Developer
166
+ * agents faced with exactly this gate concluded it was a false positive and
167
+ * committed with `--no-verify` to get past their hooks. Naming it a tool bug
168
+ * converts "agent improvises around a nonsense gate" into "agent escalates,"
169
+ * which is the behavior the gate architecture assumes.
170
+ *
171
+ * Two arms, because the edge can carry either shape: a resolved Issue number
172
+ * equal to this task's own, or — the live case — a bare same-tranche id equal
173
+ * to this task's own id.
174
+ */
175
+ function isSelfDependency(dep: DispatchDependsOnFact, task: Task, issue: DispatchIssueFact): boolean {
176
+ if (dep.issue !== null && issue !== null && dep.issue === issue.number) return true
177
+ const bare = dep.id.trim().replace(/^#/, '')
178
+ return bare.toLowerCase() === task.id.trim().toLowerCase()
179
+ }
180
+
148
181
  export function checkDispatchReadiness(input: DispatchGateInput): DispatchResult {
149
182
  const { trancheSlug, task } = input
150
183
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`
@@ -174,6 +207,17 @@ export function checkDispatchReadiness(input: DispatchGateInput): DispatchResult
174
207
  // `vinaya-engine-v1` 21, #99): a second, narrower path for a dependency
175
208
  // Issue closed directly rather than via a merged PR.
176
209
  for (const dep of input.dependsOn) {
210
+ // Evaluated BEFORE the unresolvable branch: a self-reference that also
211
+ // failed to resolve is still a parser bug, and reporting it as an
212
+ // unresolvable edge would send the reader to correct edge text that is
213
+ // not actually the fault.
214
+ if (isSelfDependency(dep, task, input.issue)) {
215
+ const issueStr = input.issue !== null ? `#${input.issue.number}` : '?'
216
+ blockers.push(
217
+ `dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`
218
+ )
219
+ continue
220
+ }
177
221
  if (dep.resolved === false) {
178
222
  // Distinct from the "not merged yet" branch below (#196): this edge
179
223
  // never resolved to any tranche/task/Issue at all, so a "not merged"