session-orchestrator 3.16.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +130 -412
  6. package/README.md +17 -12
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/docs/README.md +2 -1
  21. package/docs/USER-GUIDE.md +8 -3
  22. package/docs/ci-setup.md +121 -7
  23. package/docs/codex-setup.md +1 -1
  24. package/docs/components.md +7 -7
  25. package/docs/cursor-setup.md +22 -9
  26. package/docs/events-schema.md +5 -1
  27. package/docs/instruction-delivery.md +444 -0
  28. package/docs/pi-setup.md +1 -1
  29. package/docs/rule-authoring.md +58 -9
  30. package/docs/session-config-reference.md +306 -6
  31. package/docs/session-config-template.md +66 -3
  32. package/docs/telemetry/telemetry-claims.md +204 -0
  33. package/docs/telemetry.md +158 -0
  34. package/hooks/_lib/guard-source-loader.mjs +467 -0
  35. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  36. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  37. package/hooks/config-protection.mjs +0 -0
  38. package/hooks/enforce-commands.mjs +10 -2
  39. package/hooks/hooks-codex.json +1 -1
  40. package/hooks/hooks-cursor.json +11 -2
  41. package/hooks/hooks-pi.json +10 -0
  42. package/hooks/hooks.json +21 -1
  43. package/hooks/on-session-end.mjs +178 -18
  44. package/hooks/on-session-start.mjs +23 -0
  45. package/hooks/post-bash-write-verify.mjs +977 -0
  46. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  47. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  48. package/hooks/pre-bash-issue-budget.mjs +167 -0
  49. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  50. package/hooks/pre-bash-templates-first.mjs +96 -63
  51. package/hooks/skill-invocation-telemetry.mjs +109 -10
  52. package/hooks/subagent-telemetry.mjs +527 -37
  53. package/package.json +16 -3
  54. package/pi/prompts/contract-version-bump.md +12 -0
  55. package/rules/README.md +32 -0
  56. package/scripts/archive-closed-prds.mjs +12 -22
  57. package/scripts/autopilot-multi.mjs +103 -20
  58. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  59. package/scripts/check-doc-consistency.sh +17 -1
  60. package/scripts/compute-grounding-injection.sh +18 -3
  61. package/scripts/dialectic-deriver.mjs +7 -2
  62. package/scripts/eval-session.mjs +50 -9
  63. package/scripts/fleet-instruction-scan.mjs +141 -0
  64. package/scripts/lib/auto-dialectic.mjs +11 -2
  65. package/scripts/lib/auto-dream.mjs +16 -5
  66. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  67. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  68. package/scripts/lib/backlog-scan.mjs +39 -6
  69. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  70. package/scripts/lib/build-live-signals.mjs +7 -4
  71. package/scripts/lib/ci-status-banner.mjs +75 -12
  72. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  73. package/scripts/lib/command-blocker.mjs +1013 -58
  74. package/scripts/lib/config/config-protection.mjs +2 -1
  75. package/scripts/lib/config/context-coverage.mjs +82 -0
  76. package/scripts/lib/config/drift-check.mjs +9 -1
  77. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  78. package/scripts/lib/config/issue-budget.mjs +123 -0
  79. package/scripts/lib/config/moc-staleness.mjs +98 -0
  80. package/scripts/lib/config/reconcile.mjs +21 -0
  81. package/scripts/lib/config/section-extractor.mjs +121 -1
  82. package/scripts/lib/config/worktree-orphans.mjs +138 -0
  83. package/scripts/lib/config-schema.mjs +23 -3
  84. package/scripts/lib/config.mjs +32 -0
  85. package/scripts/lib/context-coverage-banner.mjs +223 -0
  86. package/scripts/lib/convergence-monitor.mjs +49 -3
  87. package/scripts/lib/description-surface.mjs +535 -0
  88. package/scripts/lib/dispatcher/enumerate.mjs +136 -30
  89. package/scripts/lib/dispatcher/rank.mjs +22 -8
  90. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  91. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  92. package/scripts/lib/eval/engine.mjs +47 -5
  93. package/scripts/lib/events.mjs +59 -7
  94. package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
  95. package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
  96. package/scripts/lib/gates/gate-full.mjs +15 -3
  97. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  98. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  99. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  100. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  101. package/scripts/lib/handover-gate.mjs +7 -3
  102. package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
  103. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  104. package/scripts/lib/io.mjs +345 -10
  105. package/scripts/lib/issue-budget.mjs +269 -0
  106. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  107. package/scripts/lib/label-scope.mjs +47 -0
  108. package/scripts/lib/learnings/schema.mjs +43 -3
  109. package/scripts/lib/lock-reaper.mjs +1 -2
  110. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  111. package/scripts/lib/moc-staleness-banner.mjs +267 -0
  112. package/scripts/lib/peer-discovery.mjs +645 -0
  113. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  114. package/scripts/lib/product-repo-detect.mjs +9 -8
  115. package/scripts/lib/project-hygiene.mjs +432 -0
  116. package/scripts/lib/quality-gate.mjs +167 -0
  117. package/scripts/lib/recommendations-v0.mjs +1 -1
  118. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  119. package/scripts/lib/reconcile/emitter.mjs +23 -4
  120. package/scripts/lib/reconcile/engine.mjs +147 -39
  121. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  122. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  123. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  124. package/scripts/lib/resource-probe.mjs +19 -0
  125. package/scripts/lib/rule-loader.mjs +6 -0
  126. package/scripts/lib/scope-baseline.mjs +564 -0
  127. package/scripts/lib/scope-gate.mjs +399 -98
  128. package/scripts/lib/session-close-backfill.mjs +61 -6
  129. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  130. package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
  131. package/scripts/lib/session-id.mjs +221 -41
  132. package/scripts/lib/session-lock.mjs +304 -6
  133. package/scripts/lib/session-schema/constants.mjs +22 -3
  134. package/scripts/lib/session-schema/filters.mjs +88 -0
  135. package/scripts/lib/session-schema/validator.mjs +16 -0
  136. package/scripts/lib/session-schema.mjs +1 -0
  137. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  138. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  139. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  140. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  141. package/scripts/lib/skill-health/join.mjs +35 -9
  142. package/scripts/lib/spiral-carryover.mjs +142 -30
  143. package/scripts/lib/state-md/mission-status.mjs +53 -3
  144. package/scripts/lib/subagents-schema.mjs +43 -9
  145. package/scripts/lib/telemetry/anon-id.mjs +141 -0
  146. package/scripts/lib/telemetry/consent.mjs +299 -0
  147. package/scripts/lib/telemetry/paths.mjs +27 -0
  148. package/scripts/lib/telemetry/queue.mjs +287 -0
  149. package/scripts/lib/telemetry/schema.mjs +384 -0
  150. package/scripts/lib/telemetry/sync.mjs +312 -0
  151. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  152. package/scripts/lib/tests-src-ratio.mjs +484 -0
  153. package/scripts/lib/validate/check-agents.mjs +56 -0
  154. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  155. package/scripts/lib/validate/check-rules.mjs +217 -35
  156. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  157. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  158. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  159. package/scripts/lib/vault-archive.mjs +17 -2
  160. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  161. package/scripts/lib/vault-mirror/process.mjs +30 -0
  162. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  163. package/scripts/lib/vault-status/board-writer.mjs +63 -5
  164. package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
  165. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  166. package/scripts/lib/wave-resource-gate.mjs +115 -11
  167. package/scripts/lib/worktree/listing.mjs +44 -7
  168. package/scripts/mcp-server.sh +32 -6
  169. package/scripts/measure-context-overhead.sh +151 -0
  170. package/scripts/memory-propose.mjs +72 -9
  171. package/scripts/print-applicable-rules.mjs +51 -12
  172. package/scripts/release.mjs +534 -0
  173. package/scripts/run-quality-gate.mjs +123 -5
  174. package/scripts/telemetry.mjs +250 -0
  175. package/scripts/validate-wave-scope.mjs +182 -17
  176. package/scripts/vault-integration-watcher.mjs +32 -10
  177. package/skills/_shared/config-reading.md +2 -2
  178. package/skills/bootstrap/fast-template.md +1 -1
  179. package/skills/claude-md-drift-check/checker.mjs +145 -28
  180. package/skills/contract-version-bump/SKILL.md +219 -0
  181. package/skills/discovery/SKILL.md +4 -4
  182. package/skills/discovery/issue-templates.md +11 -11
  183. package/skills/discovery/probes-audit.md +1 -1
  184. package/skills/discovery/probes-feature.md +1 -1
  185. package/skills/discovery/probes-session.md +26 -5
  186. package/skills/ecosystem-health/SKILL.md +1 -1
  187. package/skills/ecosystem-health/wizard.md +4 -4
  188. package/skills/evolve/SKILL.md +1 -0
  189. package/skills/gitlab-ops/SKILL.md +20 -12
  190. package/skills/gitlab-portfolio/SKILL.md +2 -2
  191. package/skills/hook-development/SKILL.md +1 -1
  192. package/skills/mode-selector/SKILL.md +1 -1
  193. package/skills/npm-publish/SKILL.md +97 -0
  194. package/skills/plan/SKILL.md +5 -5
  195. package/skills/plan/mode-feature.md +4 -4
  196. package/skills/plan/mode-new.md +10 -10
  197. package/skills/plan/mode-retro.md +1 -1
  198. package/skills/quality-gates/SKILL.md +1 -1
  199. package/skills/reconcile/SKILL.md +21 -4
  200. package/skills/session-end/SKILL.md +108 -14
  201. package/skills/session-end/discovery-scan.md +4 -2
  202. package/skills/session-end/drift-operations.md +4 -4
  203. package/skills/session-end/metrics-collection.md +13 -0
  204. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  205. package/skills/session-end/phase-3-6-tail.md +2 -1
  206. package/skills/session-end/plan-verification.md +5 -2
  207. package/skills/session-end/vault-operations.md +1 -1
  208. package/skills/session-end/verification-checklist.md +1 -1
  209. package/skills/session-plan/SKILL.md +6 -2
  210. package/skills/session-plan/wave-template.md +2 -0
  211. package/skills/session-start/SKILL.md +149 -7
  212. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  213. package/skills/test-runner/SKILL.md +2 -2
  214. package/skills/vault-sync/SKILL.md +1 -1
  215. package/skills/vault-sync/package-lock.json +3 -3
  216. package/skills/vault-sync/validator.mjs +228 -40
  217. package/skills/wave-executor/SKILL.md +5 -2
  218. package/skills/wave-executor/circuit-breaker.md +2 -0
  219. package/skills/wave-executor/wave-loop.md +163 -10
  220. package/templates/_shared/loop.md +4 -4
@@ -34,7 +34,10 @@ import { execFileSync } from 'node:child_process';
34
34
  import { createRequire } from 'node:module';
35
35
  import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
36
36
  import { _parseVaultIntegration } from '../../scripts/lib/config/vault-integration.mjs';
37
+ import { _parseDriftCheck } from '../../scripts/lib/config/drift-check.mjs';
38
+ import { isSessionConfigHeading } from '../../scripts/lib/config/section-extractor.mjs';
37
39
  import { parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
40
+ import { resolveRepoSpec } from '../../scripts/lib/vcs-repo-spec.mjs';
38
41
 
39
42
  const FORWARD_HEADING_RE =
40
43
  /(?:^|\b)(what'?s?\s+next|backlog|open\s+issues?|offene\s+(?:issues?|themen)|todo|next\s+steps?|roadmap)(?:$|\b)/i;
@@ -44,6 +47,10 @@ const BACKWARD_HEADING_RE =
44
47
  function parseArgs(argv) {
45
48
  const out = {
46
49
  mode: 'warn',
50
+ // #864: true only when --mode was actually passed on argv. Distinguishes
51
+ // "operator explicitly requested warn" from "the flag was never given" —
52
+ // the latter is where the target's own drift-check.mode default applies.
53
+ modeExplicit: false,
47
54
  includePaths: [],
48
55
  skipPathResolver: false,
49
56
  skipProjectCount: false,
@@ -62,7 +69,7 @@ function parseArgs(argv) {
62
69
  };
63
70
  for (let i = 0; i < argv.length; i++) {
64
71
  const a = argv[i];
65
- if (a === '--mode') out.mode = argv[++i];
72
+ if (a === '--mode') { out.mode = argv[++i]; out.modeExplicit = true; }
66
73
  else if (a === '--include-path') out.includePaths.push(argv[++i]);
67
74
  else if (a === '--repo') out.repo = argv[++i];
68
75
  else if (a === '--commands-dir') out.commandsDir = argv[++i];
@@ -79,7 +86,7 @@ function parseArgs(argv) {
79
86
  else if (a === '--skip-rule-scoping') out.skipRuleScoping = true;
80
87
  else if (a === '--skip-docs-parity') out.skipDocsParity = true;
81
88
  else if (a === '--help' || a === '-h') {
82
- process.stdout.write('Usage: checker.mjs [--mode hard|warn|off] [--include-path GLOB]... [--repo OWNER/NAME] [--commands-dir PATH] [--config-template PATH] [--skip-surface-count] [--skip-command-count] [--skip-generated-rule-staleness] [--skip-rule-scoping] [--skip-docs-parity] [--skip-*]\n');
89
+ process.stdout.write('Usage: checker.mjs [--mode strict|warn|off] [--include-path GLOB]... [--repo OWNER/NAME] [--commands-dir PATH] [--config-template PATH] [--skip-surface-count] [--skip-command-count] [--skip-generated-rule-staleness] [--skip-rule-scoping] [--skip-docs-parity] [--skip-*]\n --mode precedence (issue #864): explicit --mode > target CLAUDE.md/AGENTS.md drift-check.mode > \'warn\'\n');
83
90
  process.exit(0);
84
91
  } else {
85
92
  process.stderr.write(`{"status":"infra-error","reason":"unknown arg: ${a}"}\n`);
@@ -146,7 +153,14 @@ function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
146
153
  const lines = content.split('\n');
147
154
  const headingIdxs = [];
148
155
  for (let i = 0; i < lines.length; i++) {
149
- if (/^##\s+Session Config\b/.test(lines[i])) headingIdxs.push(i);
156
+ // SSOT predicate (#968). The previous local `/^##\s+Session Config\b/`
157
+ // was the loosest JS comparator in the repo: no end anchor and `\s+`
158
+ // instead of a single space, so it accepted `## Session Config` (two
159
+ // spaces) and `## Session Config Convention` — both of which the runtime
160
+ // parser rejects. This checker's whole job is to report on the block the
161
+ // runtime reads, so matching a heading the runtime cannot see made it
162
+ // audit a section that does not exist.
163
+ if (isSessionConfigHeading(lines[i])) headingIdxs.push(i);
150
164
  }
151
165
  if (headingIdxs.length === 0) return null;
152
166
 
@@ -172,10 +186,23 @@ function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
172
186
  /**
173
187
  * Extract top-level YAML keys from a YAML body. Only column-0 keys are
174
188
  * collected (indented keys are children and ignored).
189
+ *
190
+ * Accepts four equivalent forms consumer CLAUDE.md files write a Session
191
+ * Config key in: bare `key:`, Markdown bullet `- key:`, bold `**key:**`
192
+ * (closing bold before the colon), and the bold-bullet consumer shape
193
+ * `- **key:** value` (bold wraps the key AND colon together, closing bold
194
+ * AFTER the colon). Baseline issue #60: the original bare-only regex
195
+ * extracted zero keys from bullet-form local files, so session-config-parity
196
+ * (Check 6, below) either false-positived on every mandatory key (local side
197
+ * reads as empty) or, when the TEMPLATE side also used bullet form, passed
198
+ * vacuously (both sides empty, so the diff was always empty). Sibling
199
+ * precedent: `scripts/lib/config/block-header.mjs` recognizes only header
200
+ * lines, not individual key-value pairs — this function is the per-key
201
+ * counterpart.
175
202
  */
176
203
  function extractTopLevelKeys(body) {
177
204
  const keys = [];
178
- const re = /^([A-Za-z][\w-]*):/gm;
205
+ const re = /^(?:-\s+)?(?:\*\*)?([A-Za-z][\w-]*)(?:\*\*)?:/gm;
179
206
  let m;
180
207
  while ((m = re.exec(body)) !== null) {
181
208
  keys.push(m[1]);
@@ -416,13 +443,20 @@ function buildSurfaceDescriptors(vaultDir, commandsDir) {
416
443
  ];
417
444
  }
418
445
 
419
- const REPO_SHAPE_RE = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/;
420
-
421
- function detectRepo(vaultDir) {
422
- const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: vaultDir, encoding: 'utf8' }).trim();
423
- const m = /[:/]([^:/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(url);
424
- const candidate = m ? m[1] : null;
425
- return candidate && REPO_SHAPE_RE.test(candidate) ? candidate : null;
446
+ /**
447
+ * Guard for a `--repo`/`-R` argv value before it is passed to `glab`
448
+ * (host-pinning, #872). Replaces the old `REPO_SHAPE_RE` "owner/repo" shape
449
+ * check `resolveRepoSpec` for `vcs: 'gitlab'` returns the raw remote URL
450
+ * (which `glab -R`/`--repo` explicitly accepts), so the guard only needs to
451
+ * reject the same argv-corrupting characters checked elsewhere in this repo
452
+ * (see `ARG_BOUNDARY_DANGEROUS` in scripts/lib/test-runner/issue-reconcile.mjs):
453
+ * non-empty and free of whitespace/newlines.
454
+ *
455
+ * @param {unknown} spec
456
+ * @returns {boolean}
457
+ */
458
+ function isSafeRepoSpec(spec) {
459
+ return typeof spec === 'string' && spec.length > 0 && !/\s/.test(spec);
426
460
  }
427
461
 
428
462
  function hasGlab() {
@@ -432,11 +466,18 @@ function hasGlab() {
432
466
  } catch { return false; }
433
467
  }
434
468
 
435
- function lookupIssueState(iid, repo, cache) {
469
+ function lookupIssueState(iid, repo, cache, vaultDir) {
436
470
  if (cache.has(iid)) return cache.get(iid);
437
471
  let state = 'unknown';
438
472
  try {
473
+ // #872: cwd: vaultDir — without it, a bare `cwd`-less spawn resolves the
474
+ // ambient GITLAB_HOST relative to the PROCESS cwd (not vaultDir), which
475
+ // can silently target the wrong instance on a multi-instance host even
476
+ // though `--repo <repo>` is already present (host-pinning consistency
477
+ // with detectRepo's git-remote resolution above, which DOES use
478
+ // `cwd: vaultDir` via resolveRepoSpec).
439
479
  const out = execFileSync('glab', ['issue', 'view', iid, '--repo', repo], {
480
+ cwd: vaultDir,
440
481
  encoding: 'utf8',
441
482
  stdio: ['ignore', 'pipe', 'pipe'],
442
483
  });
@@ -450,8 +491,11 @@ function lookupIssueState(iid, repo, cache) {
450
491
  // ───────────────────────────────────────────────────────────────────────────
451
492
  // Rule-scoping family (Check 9) — validates .claude/rules/*.md frontmatter
452
493
  // against the scripts/lib/rule-loader.mjs contract:
453
- // 1. paths-presence → errors[]: a top-level `paths:` key (rule-loader.mjs
454
- // only recognises `globs:`; `paths:` silently loads the rule ALWAYS-ON).
494
+ // 1. paths-presence → errors[]: a top-level `paths:` key that
495
+ // rule-loader.mjs's parseGlobsFrontmatter (the `paths:` alias, issue
496
+ // #795) fails to recognise — a genuine parse mismatch. A well-formed
497
+ // `paths:`-only rule is NOT flagged: since #795, `paths:` is a full
498
+ // alias for `globs:` and loads correctly SCOPED (#840).
455
499
  // 2. cited-but-missing → errors[]: (a) `.claude/rules/<name>.md` citations in
456
500
  // CLAUDE.md/AGENTS.md that don't exist on disk; (b) bare `<name>.md`
457
501
  // tokens in a rule's own `## See Also` footer that don't exist on disk
@@ -585,18 +629,70 @@ function main() {
585
629
  const args = parseArgs(process.argv.slice(2));
586
630
  const vaultDir = resolve(process.env.VAULT_DIR || process.cwd());
587
631
 
588
- if (!['hard', 'warn', 'off'].includes(args.mode)) {
589
- process.stderr.write(`{"status":"infra-error","reason":"invalid --mode: ${args.mode}"}\n`);
590
- process.exit(2);
591
- }
592
-
593
- // Alias-aware instruction file resolution (issue #33).
632
+ // Alias-aware instruction file resolution (issue #33) — resolved BEFORE
633
+ // mode defaulting so an unspecified --mode can read the target's OWN
634
+ // drift-check.mode (issue #864).
594
635
  // CLAUDE.md (Claude Code / Cursor IDE) and AGENTS.md (Codex CLI) are
595
636
  // transparent aliases — see skills/_shared/instruction-file-resolution.md.
596
637
  const instr = resolveInstructionFile(vaultDir); // {path, kind} | null
597
638
  const resolvedPath = instr ? instr.path : null;
598
639
  const resolvedKind = instr ? instr.kind : null;
599
640
 
641
+ // #864: precedence is --mode flag > target's drift-check.mode > 'warn'.
642
+ // An invocation without an explicit --mode previously always ran in the
643
+ // hardcoded 'warn' default even when the target's own Session Config
644
+ // declared `drift-check.mode: strict` — silently softening the very
645
+ // contract the config was meant to enforce. Only substitute when --mode
646
+ // was never passed on argv; an explicit flag always wins. `_parseDriftCheck`
647
+ // is the SAME parser scripts/lib/config.mjs uses elsewhere, so this reuses
648
+ // rather than re-derives the drift-check block's mode-parsing/normalization
649
+ // (including its own hard→strict alias and its 'warn' fallback on absence
650
+ // or an invalid value).
651
+ //
652
+ // Blast-radius guard (QA follow-up on #864, reproduced empirically):
653
+ // the auto-read must not apply AT ALL when the feature it belongs to is
654
+ // disabled — `drift-check.enabled: false` means the operator switched the
655
+ // whole gate off, and a stale/dormant `mode: strict` sitting in that
656
+ // disabled block must not hard-block a bare invocation. Read `enabled` off
657
+ // the SAME `_parseDriftCheck()` call (it already parses the flag) rather
658
+ // than a second bespoke regex.
659
+ //
660
+ // Separately, a config-derived `mode: off` is never auto-applied even when
661
+ // `enabled: true`: 'off' is a legitimate silencing only when a human
662
+ // explicitly types `--mode off` on the CLI. Picking it up silently from
663
+ // config would turn what used to be a full warn-mode report into an
664
+ // undetectable no-op on every future bare invocation — the exact silent
665
+ // failure class `.claude/rules/verification-before-completion.md` § VBC-005
666
+ // exists to prevent. Concretely: config-derived mode is applied only when
667
+ // `enabled === true` AND `mode !== 'off'`; a config `mode: off` downgrades
668
+ // to the built-in 'warn' default instead of taking effect. This keeps
669
+ // `status: 'skipped-mode-off'` reachable ONLY via an explicit `--mode off`
670
+ // flag — a caller who never passed that flag can rely on a bare invocation
671
+ // always actually running its checks, even against a config that declares
672
+ // `mode: off`.
673
+ if (!args.modeExplicit && instr) {
674
+ try {
675
+ const targetDriftCheck = _parseDriftCheck(readFileSync(instr.path, 'utf8'));
676
+ if (targetDriftCheck.enabled && targetDriftCheck.mode !== 'off') {
677
+ args.mode = targetDriftCheck.mode;
678
+ }
679
+ } catch {
680
+ // target file unreadable (race, permissions) — keep the 'warn' CLI default
681
+ }
682
+ }
683
+
684
+ if (!['strict', 'hard', 'warn', 'off'].includes(args.mode)) {
685
+ process.stderr.write(`{"status":"infra-error","reason":"invalid --mode: ${args.mode}"}\n`);
686
+ process.exit(2);
687
+ }
688
+ // `hard` is a legacy alias for `strict` (#217 enum migration — parity with
689
+ // the vault-sync validator, which normalizes the reverse direction). Collapse
690
+ // to a single blocking value so exactly one internal value flows downstream.
691
+ // (`_parseDriftCheck` already normalizes 'hard'->'strict' on the config-default
692
+ // path above, so this is a no-op there — it remains load-bearing for an
693
+ // explicit `--mode hard` on the CLI.)
694
+ if (args.mode === 'hard') args.mode = 'strict';
695
+
600
696
  if (args.mode === 'off') {
601
697
  process.stdout.write(JSON.stringify({
602
698
  status: 'skipped-mode-off', mode: 'off', vault_dir: vaultDir,
@@ -657,13 +753,20 @@ function main() {
657
753
  const commandSurface = activeSurfaces.find((s) => s.id === 'command-count');
658
754
  const actualCommandCount = commandSurface ? commandSurface.actual : null;
659
755
 
756
+ // #872: CLI --repo always wins; otherwise auto-detect a --repo spec from
757
+ // the local git remotes (host-pinning — a bare glab spawn falls back to
758
+ // the ambient GITLAB_HOST, which can silently target the wrong instance
759
+ // on a multi-instance host).
660
760
  let repo = args.repo;
661
761
  const glabPresent = !args.skipIssueRefs && hasGlab();
662
762
  if (!args.skipIssueRefs && !glabPresent) {
663
763
  checksSkipped.push('issue-reference-freshness: glab not found in PATH');
664
764
  }
665
765
  if (!args.skipIssueRefs && glabPresent && !repo) {
666
- try { repo = detectRepo(vaultDir); } catch { /* ignore */ }
766
+ try {
767
+ const resolved = resolveRepoSpec({ repoRoot: vaultDir, vcs: 'gitlab' });
768
+ repo = resolved && isSafeRepoSpec(resolved) ? resolved : null;
769
+ } catch { /* ignore */ }
667
770
  if (!repo) checksSkipped.push('issue-reference-freshness: could not detect origin repo (use --repo)');
668
771
  }
669
772
  const runIssueCheck = !args.skipIssueRefs && glabPresent && !!repo;
@@ -979,18 +1082,32 @@ function main() {
979
1082
  }
980
1083
 
981
1084
  // --- Probe 1: paths-presence → errors[] ---
1085
+ // Since ef7f4fc (2026-07-13, issue #795), rule-loader.mjs's
1086
+ // parseGlobsFrontmatter() accepts `paths:` as a full alias for
1087
+ // `globs:` (globs: wins silently only when BOTH keys are present on
1088
+ // the same rule — see rule-loader.mjs module doc). A well-formed
1089
+ // `paths:`-only rule therefore loads correctly SCOPED, not
1090
+ // always-on — declaring `paths:` alone must NOT be flagged (#840).
1091
+ // This probe reuses parseGlobsFrontmatter — the SAME parser
1092
+ // rule-loader.mjs itself runs — rather than re-deriving the alias
1093
+ // rule with a second hand-rolled regex; duplicating a loader's
1094
+ // parsing logic is exactly the drift class that caused #840. It
1095
+ // fires only when the textual `paths:` key is present yet
1096
+ // parseGlobsFrontmatter failed to recognise it (globs === null) —
1097
+ // a genuine parse mismatch between this probe's textual detection
1098
+ // and the loader's actual behaviour, not routine `paths:` usage.
1099
+ let parsed;
1100
+ try { parsed = parseGlobsFrontmatter(content); } catch { parsed = { globs: null, meta: {} }; }
982
1101
  const fmBody = extractFrontmatterBlockBody(content);
983
- if (fmBody && /^paths:/m.test(fmBody)) {
1102
+ if (fmBody && /^paths:/m.test(fmBody) && parsed.globs === null) {
984
1103
  errors.push({
985
1104
  check: 'rule-scoping', file: relPath, line: 1,
986
- message: `Rule frontmatter declares 'paths:' which rule-loader.mjs does not recognise — the rule silently loads ALWAYS-ON regardless of file scope. Migrate to 'globs:'.`,
1105
+ message: `Rule frontmatter declares 'paths:' but rule-loader.mjs's parseGlobsFrontmatter did not recognise it — the rule may silently load ALWAYS-ON. Verify the paths:/globs: frontmatter syntax.`,
987
1106
  extracted: 'paths:',
988
1107
  });
989
1108
  }
990
1109
 
991
1110
  // --- Probes 3 & 4: zero-match-globs / foreign-glob → warnings[] ---
992
- let parsed;
993
- try { parsed = parseGlobsFrontmatter(content); } catch { parsed = { globs: null, meta: {} }; }
994
1111
  const globs = parsed.globs;
995
1112
  if (Array.isArray(globs) && globs.length > 0) {
996
1113
  if (trackedFiles === null) trackedFiles = listTrackedFiles(vaultDir);
@@ -1212,7 +1329,7 @@ function main() {
1212
1329
  files_scanned: 0, checks_run: checksRun, checks_skipped: checksSkipped,
1213
1330
  errors, warnings, reason: 'no scope files matched',
1214
1331
  }) + '\n');
1215
- process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
1332
+ process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);
1216
1333
  }
1217
1334
 
1218
1335
  for (const abs of scopeFiles) {
@@ -1272,7 +1389,7 @@ function main() {
1272
1389
  let m;
1273
1390
  while ((m = issueRegex.exec(line)) !== null) {
1274
1391
  const iid = m[1];
1275
- const state = lookupIssueState(iid, repo, issueCache);
1392
+ const state = lookupIssueState(iid, repo, issueCache, vaultDir);
1276
1393
  if (state === 'closed') {
1277
1394
  errors.push({
1278
1395
  check: 'issue-reference-freshness', file: rel, line: lineNum,
@@ -1374,7 +1491,7 @@ function main() {
1374
1491
  }
1375
1492
  process.stdout.write(JSON.stringify(result) + '\n');
1376
1493
 
1377
- process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
1494
+ process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);
1378
1495
  }
1379
1496
 
1380
1497
  main();
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: contract-version-bump
3
+ user-invocable: true
4
+ tags: [contracts, versioning, schema, changelog, governance]
5
+ model: sonnet
6
+ model-preference: sonnet
7
+ model-preference-codex: gpt-5.4-mini
8
+ model-preference-cursor: claude-sonnet-4-6
9
+ args-schema:
10
+ - flag: --contract <path>
11
+ description: "Path to the contract file being bumped (JSON Schema, API spec, config schema). Inferred from conversation context if omitted."
12
+ description: >
13
+ Use this skill when changing a machine-readable contract — a JSON Schema, an API spec, or a
14
+ config schema — and bumping its version: tightening a constraint, adding/removing/renaming a
15
+ field, introducing a breaking change, raising an API version, or writing the changelog entry
16
+ for a schema change. Trigger on "change the schema", "tighten this constraint", "bump the
17
+ schema version", "breaking change to the API", "new API version", "changelog entry for a
18
+ schema change". Runs six phases — classify against the contract's OWN versioning rule (not
19
+ generic semver instinct), find every version literal and vendored copy, check consumer
20
+ compatibility for new keywords, apply consistently, write the changelog entry, and report
21
+ downstream drift — codifying three non-obvious traps hit in a real case (GitLab issue #17,
22
+ `aiat-enablement` repo, 2026-07-25).
23
+ ---
24
+
25
+ # Contract Version Bump
26
+
27
+ > Project-instruction file resolution: `CLAUDE.md` and `AGENTS.md` (Codex CLI) are transparent
28
+ > aliases — see [skills/_shared/instruction-file-resolution.md](../_shared/instruction-file-resolution.md).
29
+ > Every reference to `CLAUDE.md` in this skill resolves via that precedence rule.
30
+
31
+ A version bump on a machine-readable contract (JSON Schema, OpenAPI/API spec, config schema) is
32
+ not just "increment the number." Three failure modes recur and are each individually
33
+ non-obvious enough that a careful agent still misses them without a checklist: the contract's
34
+ own versioning rule may not even cover the change you're making; version literals live in more
35
+ places than you remember; and a schema keyword that no consumer evaluates is worse than no
36
+ constraint at all, because it *looks* enforced. This skill runs six phases to close all three.
37
+
38
+ **Reference case:** `aiat-enablement` repo, GitLab issue #17 (2026-07-25). A JSON Schema contract
39
+ (`docs/spec/estate.schema.json`, draft/2020-12) had six fields tightened with new `pattern`
40
+ constraints; `schema_version` moved `0.1.0` → `0.1.1`. All three traps below happened in that
41
+ one change. Where cited, "the reference case" means this.
42
+
43
+ ## Phase 1: Classify against the CONTRACT'S OWN versioning rule
44
+
45
+ Do not classify Patch/Minor/Major from generic semver instinct. Read the contract's own
46
+ versioning-rules section (in its prose spec, README, or a `## Versioning` block near the schema)
47
+ and ask explicitly: **does this rule even define the change class you're making?**
48
+
49
+ ```bash
50
+ # Does the spec define rules for Patch changes at all? (substitute Minor/Major as needed)
51
+ grep -n "^#.*[Vv]ersion" docs/spec/<contract>.md
52
+ grep -c "Patch" docs/spec/<contract>.md
53
+ ```
54
+
55
+ If the clause you need is missing, **authoring that clause is part of this change**, not a
56
+ side quest — write it with an explicit justification for why the new class exists and why your
57
+ change belongs in it, then proceed. If the clause exists but classifies your change as a stricter
58
+ tier than you assumed (e.g. a "just tightening a pattern" edit is actually forbidden under an
59
+ additive-only Minor rule), the rule wins — either recategorize the change or amend the rule
60
+ first, but never bump the number past what the contract's own rule permits.
61
+
62
+ Reference case: `docs/spec/estate-yaml-v0.md` defined only Minor (additive-only) and Major
63
+ (needs a migration step). `grep -c "Patch" docs/spec/estate-yaml-v0.md` returned `0`. A
64
+ constraint tightening is not additive, so under the existing rule alone it was forbidden. The
65
+ Patch clause had to be authored — with a written justification (no production consumers yet
66
+ validate against this vorproduktions-schema, so no migration burden exists) — before the bump
67
+ was legitimate.
68
+
69
+ ## Phase 2: Find every version literal and copy — mechanically, not from memory
70
+
71
+ Never trust "I updated it everywhere I remember." Grep for the literal.
72
+
73
+ ```bash
74
+ # 1. Every occurrence of the current version string in this repo
75
+ grep -rn "<current-version>" --include=*.json --include=*.md --include=*.yaml --include=*.yml .
76
+
77
+ # 2. The field/key that carries the version, wherever it's mentioned in prose
78
+ # (field-catalog tables, example fixtures, README snippets)
79
+ grep -rln "schema_version\|apiVersion\|<version-field-name>" docs/ examples/ 2>/dev/null
80
+
81
+ # 3. Vendored copies in sibling repos — check every path under the instruction
82
+ # file's `cross-repos:` list. Match by basename, not by path: a vendored copy
83
+ # is rarely at an identical relative path.
84
+ # CLAUDE.md and AGENTS.md are transparent aliases — resolve whichever exists.
85
+ INSTR=$([ -f CLAUDE.md ] && echo CLAUDE.md || echo AGENTS.md)
86
+ for repo in $(yq '.["cross-repos"][]' "$INSTR" 2>/dev/null || grep -A20 '^cross-repos:' "$INSTR" | grep ' - ' | sed 's/^ *- *//'); do
87
+ find "$repo" -iname "$(basename <contract-file>)" 2>/dev/null
88
+ done
89
+ ```
90
+
91
+ For every location found, decide explicitly: **does it get bumped, or is it exempt?** Exemptions
92
+ are legitimate (a dated SSOT snapshot like a PRD is allowed to stay frozen at the version it was
93
+ approved under) but the exemption must be written down next to the literal that was skipped, or
94
+ the next diff will read as silent divergence.
95
+
96
+ Reference case: `schema_version` lived in five places — the schema file itself
97
+ (`docs/spec/estate.schema.json`), the field-catalog table in the prose spec
98
+ (`docs/spec/estate-yaml-v0.md`), an example fixture (`docs/spec/examples/estate.example.yaml`), a
99
+ **vendored copy in a different repo** (`aiat-poc-infra/scripts/estate/estate.schema.json`), and
100
+ the PRD (`docs/prd/2026-07-25-aiat-enablement.md`, Anhang D.1). The PRD was deliberately *not*
101
+ bumped — but that decision was written into the spec's "Anmerkungen zu diesem Dokument" section
102
+ explicitly, precisely so it would never be mistaken for an oversight.
103
+
104
+ ## Phase 3: Check consumer compatibility for every new/changed keyword
105
+
106
+ For each keyword you are adding or changing (`pattern`, `maxLength`, `enum`, `format`,
107
+ `additionalProperties`, a new required field, a new `apiVersion` value, …), find every known
108
+ consumer of the contract and ask: **does this consumer actually evaluate this keyword, or does
109
+ it silently ignore what it doesn't recognize?**
110
+
111
+ - A full-featured library (ajv, `jsonschema`, an OpenAPI-generated client) generally implements
112
+ the standard vocabulary — trust it, but confirm the vocabulary/draft version matches (e.g. a
113
+ draft-07 validator will not enforce 2020-12-only keywords).
114
+ - A hand-written parser/interpreter (a bash+heredoc validator, a custom regex-based checker, a
115
+ bespoke deserializer) is the risk case. Grep its source for the keyword name:
116
+
117
+ ```bash
118
+ grep -n "maxLength\|minLength\|pattern\|format\|enum" <consumer-script-or-module>
119
+ ```
120
+
121
+ If the keyword is absent from the consumer's implementation, you have three options — pick one
122
+ and write it down, never leave it implicit:
123
+ 1. **Extend the consumer** to support the keyword (preferred when the consumer is yours to change).
124
+ 2. **Fold the constraint into a keyword the consumer already supports** (e.g. encode a length
125
+ limit inside a `pattern` instead of a separate `maxLength`).
126
+ 3. **Accept the gap and document it as a known limitation** in the spec, naming the consumer —
127
+ only when neither of the above is feasible right now.
128
+
129
+ A constraint a consumer silently ignores is worse than no constraint: it looks enforced in the
130
+ schema, so nobody double-checks the actual runtime behavior, and invalid data passes through
131
+ undetected.
132
+
133
+ Reference case: `maxLength: 63` was the natural way to express S3 bucket-name limits. The
134
+ downstream validator (`aiat-poc-infra/scripts/estate/validate-estate-yaml.sh`) is a hand-written
135
+ JSON-Schema mini-interpreter that does not implement `maxLength` — it would have parsed the
136
+ schema, not recognized the keyword, and silently done nothing, so a 200-character bucket name
137
+ would still validate green. Decision: extend the consumer (option 1) rather than relying only on
138
+ the `pattern`'s implicit length ceiling — the `maxLength` branch was added to the interpreter in
139
+ the same session, on branch `feature/estate-yaml-format-tightening-17`, and proven by a mutation
140
+ test (disable the branch → the `[MAXLENGTH]` assertion goes red while `[PATTERN]` still fires).
141
+
142
+ That mutation test is itself the lesson: because the bucket `pattern`
143
+ (`^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$`) already bounds total length via its quantifier, an
144
+ over-length value trips BOTH rules. A fixture alone could not prove the new `maxLength` code was
145
+ load-bearing — only disabling that code and watching the specific assertion fail could. When a
146
+ new keyword overlaps an existing constraint, prove it in isolation or you have not proven it.
147
+
148
+ ## Phase 4: Apply consistently
149
+
150
+ Bump the version literal and the constraint change together, everywhere Phase 2 found a
151
+ non-exempt occurrence. For each exemption identified in Phase 2, write the reason next to it
152
+ (a spec's "Anmerkungen"/decisions section, a code comment, a linked issue) — an exemption without
153
+ a written reason is indistinguishable from a bug the next time someone diffs the two documents.
154
+
155
+ ## Phase 5: Write the changelog entry
156
+
157
+ Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions already in use in
158
+ this repo's `CHANGELOG.md` — add the entry under the repo's convention for in-progress work
159
+ (commonly an `## [Unreleased]` section; check the top of the file for the existing pattern before
160
+ inventing a new one). What goes in the entry depends on the classification from Phase 1:
161
+
162
+ - **Patch (constraint tightening on an existing field):** name every field that changed, the old
163
+ constraint and the new one. **State explicitly that this can reject previously-valid documents
164
+ for consumers, even though the version number alone does not signal a breaking change** — the
165
+ version-number tier and the actual blast radius for a consumer are two different axes; do not
166
+ let the reader infer breaking-ness from the tier alone.
167
+ - **Minor (additive field(s)):** name the new field(s) and confirm the change is additive-only —
168
+ no existing field's meaning, requiredness, or name changed.
169
+ - **Major (breaking structural change):** name the field(s)/structure that changed and link the
170
+ migration step (e.g. `migrations/<contract>/`) a consumer must run.
171
+
172
+ ## Phase 6: Report downstream drift
173
+
174
+ List every vendored copy or dependent repo found in Phase 2 that this change did **not** update.
175
+ For each: name the repo/path, why it wasn't updated now (e.g. "follow-up MR pending, tracked
176
+ separately"), and the tracking issue if one exists. Never let a known-stale copy pass silently —
177
+ surface it as explicit follow-up work, even if fixing it is out of scope for this change.
178
+
179
+ ---
180
+
181
+ ## Checklist
182
+
183
+ - [ ] Read the contract's own versioning-rule section. Does it define the change class you're
184
+ making? If not, write the clause first (with justification), before touching the version.
185
+ - [ ] Classify Patch / Minor / Major against that rule — not generic semver instinct.
186
+ - [ ] `grep -rn` the current version literal across this repo (schema, prose spec, examples/fixtures).
187
+ - [ ] Search every repo in the instruction file's `cross-repos:` list (`CLAUDE.md`, or `AGENTS.md`
188
+ on Codex CLI) for a vendored copy (basename match).
189
+ - [ ] For every new/changed keyword, grep each known consumer's source for that keyword name.
190
+ Decide: extend / fold into a supported keyword / document the gap — pick one, write it down.
191
+ - [ ] Apply the bump + constraint change to every non-exempt literal found in Phase 2.
192
+ - [ ] Write the reason next to every exemption (a document deliberately NOT bumped).
193
+ - [ ] Write the changelog entry — name the fields, old vs. new constraint, and call out breaking
194
+ risk for consumers explicitly, independent of the version tier.
195
+ - [ ] List every dependent repo/copy left un-synced, with a tracking issue.
196
+
197
+ ## Anti-Patterns
198
+
199
+ 1. **Silent contract violation.** Bumping a version for a change class the contract's own rule
200
+ doesn't cover — or actively forbids — without amending the rule first. *Reference case:* the
201
+ spec defined only Minor (additive) and Major (needs migration); a constraint tightening isn't
202
+ additive, so it was forbidden under the existing rule until a Patch clause was authored with
203
+ an explicit justification.
204
+ 2. **Literal drift from memory.** Updating the version "everywhere I remember" instead of
205
+ grepping mechanically. *Reference case:* the version literal lived in five places, including a
206
+ vendored copy in a *different* repo (`aiat-poc-infra`) that a memory-based sweep would not
207
+ have found; one location (the PRD) was correctly left un-bumped, but only because that
208
+ exemption was written down explicitly instead of left silent.
209
+ 3. **Ignored keyword, worse than no keyword.** Adding a schema keyword without checking whether
210
+ every known consumer evaluates it. *Reference case:* `maxLength: 63` would have been silently
211
+ skipped by a hand-written validator that doesn't implement it — the schema would have *looked*
212
+ enforced while the runtime check let arbitrarily long values through.
213
+
214
+ ## When this does not apply
215
+
216
+ Pure documentation-only edits to a contract (typo fixes, added examples, clarified descriptions)
217
+ that change no validated field, constraint, or version-relevant semantics do not need a version
218
+ bump or this skill — but if you're unsure whether a wording change is validation-relevant, treat
219
+ it as a change and run Phase 1.
@@ -373,7 +373,7 @@ AskUserQuestion({
373
373
  question: "<finding title>\n\n<file_path>:<line_number>\n```\n<matched_text with +/-3 lines context>\n```\n\n<description>\n\nRecommended fix: <recommended_fix>",
374
374
  header: "<severity>",
375
375
  options: [
376
- { label: "Create issue (<severity>)", description: "Create a priority:<severity> issue for this finding" },
376
+ { label: "Create issue (<severity>)", description: "Create a priority::<severity> issue for this finding" },
377
377
  { label: "Adjust priority", description: "Create issue with different priority" },
378
378
  { label: "Dismiss -- intentional", description: "This is by design, skip" },
379
379
  { label: "Dismiss -- false positive", description: "Detection was wrong, skip" }
@@ -432,10 +432,10 @@ AskUserQuestion({
432
432
  For each approved finding:
433
433
 
434
434
  1. Format using the Discovery Finding Issue Template from `issue-templates.md`
435
- 2. Determine labels: `type:discovery` + `priority:<level>` + `area:<inferred from category/filepath>` + `status:ready`
435
+ 2. Determine labels: `type:discovery` + `priority::<level>` + `area:<inferred from category/filepath>` + `status:ready`
436
436
  3. Create issue via VCS CLI:
437
- - **GitLab**: `glab issue create --title "[Discovery] <title>" --label "type:discovery,priority:<level>,area:<area>,status:ready" --description "<body>"`
438
- - **GitHub**: `gh issue create --title "[Discovery] <title>" --label "type:discovery,priority:<level>,area:<area>,status:ready" --body "<body>"`
437
+ - **GitLab**: `glab issue create --title "[Discovery] <title>" --label "type:discovery,priority::<level>,area:<area>,status:ready" --description "<body>"`
438
+ - **GitHub**: `gh issue create --title "[Discovery] <title>" --label "type:discovery,priority::<level>,area:<area>,status:ready" --body "<body>"`
439
439
  4. Brief pause (1s) between creations for rate limiting
440
440
 
441
441
  ### 6.2 Final Report
@@ -10,7 +10,7 @@ References the label taxonomy from the gitlab-ops skill.
10
10
  Discovery findings use the following labels from the gitlab-ops label taxonomy:
11
11
 
12
12
  - **Type:** `type:discovery`
13
- - **Priority:** `priority:critical` | `priority:high` | `priority:medium` | `priority:low` (mapped from probe severity)
13
+ - **Priority:** `priority::critical` | `priority::high` | `priority::medium` | `priority::low` (mapped from probe severity)
14
14
  - **Area:** `area:frontend` | `area:backend` | `area:security` | `area:testing` | `area:ci` | `area:infrastructure` (mapped from probe category)
15
15
  - **Status:** `status:ready`
16
16
 
@@ -28,10 +28,10 @@ Discovery findings use the following labels from the gitlab-ops label taxonomy:
28
28
 
29
29
  | Probe Severity | Priority Label |
30
30
  |---|---|
31
- | Critical | `priority:critical` |
32
- | High | `priority:high` |
33
- | Medium | `priority:medium` |
34
- | Low | `priority:low` |
31
+ | Critical | `priority::critical` |
32
+ | High | `priority::high` |
33
+ | Medium | `priority::medium` |
34
+ | Low | `priority::low` |
35
35
 
36
36
  ---
37
37
 
@@ -74,14 +74,14 @@ Used for a single finding from a single probe.
74
74
  - [ ] Quality gates pass after fix
75
75
  ```
76
76
 
77
- **Labels:** `type:discovery`, `priority:<level>`, `area:<area>`, `status:ready`
77
+ **Labels:** `type:discovery`, `priority::<level>`, `area:<area>`, `status:ready`
78
78
 
79
79
  **CLI Example:**
80
80
  ```bash
81
81
  # GitHub
82
82
  gh issue create \
83
83
  --title "[Discovery] <finding_title>" \
84
- --label "type:discovery,priority:<level>,area:<area>,status:ready" \
84
+ --label "type:discovery,priority::<level>,area:<area>,status:ready" \
85
85
  --body "$(cat <<'EOF'
86
86
  <template body filled in>
87
87
  EOF
@@ -90,7 +90,7 @@ EOF
90
90
  # GitLab
91
91
  glab issue create \
92
92
  --title "[Discovery] <finding_title>" \
93
- --label "type:discovery,priority:<level>,area:<area>,status:ready" \
93
+ --label "type:discovery,priority::<level>,area:<area>,status:ready" \
94
94
  --description "$(cat <<'EOF'
95
95
  <template body filled in>
96
96
  EOF
@@ -154,14 +154,14 @@ Used when multiple related findings from the same probe or category are grouped
154
154
  - [ ] Quality gates pass after fixes
155
155
  ```
156
156
 
157
- **Labels:** `type:discovery`, `priority:<highest_severity_found>`, `area:<area>`, `status:ready`
157
+ **Labels:** `type:discovery`, `priority::<highest_severity_found>`, `area:<area>`, `status:ready`
158
158
 
159
159
  **CLI Example:**
160
160
  ```bash
161
161
  # GitHub
162
162
  gh issue create \
163
163
  --title "[Discovery] <category> audit: <summary>" \
164
- --label "type:discovery,priority:<level>,area:<area>,status:ready" \
164
+ --label "type:discovery,priority::<level>,area:<area>,status:ready" \
165
165
  --body "$(cat <<'EOF'
166
166
  <template body filled in>
167
167
  EOF
@@ -170,7 +170,7 @@ EOF
170
170
  # GitLab
171
171
  glab issue create \
172
172
  --title "[Discovery] <category> audit: <summary>" \
173
- --label "type:discovery,priority:<level>,area:<area>,status:ready" \
173
+ --label "type:discovery,priority::<level>,area:<area>,status:ready" \
174
174
  --description "$(cat <<'EOF'
175
175
  <template body filled in>
176
176
  EOF
@@ -78,7 +78,7 @@ Category: <name>
78
78
 
79
79
  **Default Severity:** High if `score_0_10 < 5`; Medium if `5 <= score_0_10 < 7`.
80
80
 
81
- **Labels for auto-created issues:** `type:discovery`, `area:harness` (new label — add to VCS label set if absent), `priority:high` or `priority:medium` per severity, `status:ready`.
81
+ **Labels for auto-created issues:** `type:discovery`, `area:harness` (new label — add to VCS label set if absent), `priority::high` or `priority::medium` per severity, `status:ready`.
82
82
 
83
83
  **Skip Conditions:**
84
84
 
@@ -147,4 +147,4 @@ verification_method: vcs-issue
147
147
 
148
148
  ---
149
149
 
150
- **Labels for auto-created issues:** `type:discovery`, `area:skills`, `priority:high` (High/Critical severity) or `priority:medium` (Medium severity), `status:ready`.
150
+ **Labels for auto-created issues:** `type:discovery`, `area:skills`, `priority::high` (High/Critical severity) or `priority::medium` (Medium severity), `status:ready`.