session-orchestrator 3.17.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +105 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +244 -9
- package/docs/session-config-template.md +39 -3
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +5 -2
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -13
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +73 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- 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
|
|
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
|
-
|
|
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-]*)
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
454
|
-
//
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 {
|
|
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:'
|
|
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 === '
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
|
438
|
-
- **GitHub**: `gh issue create --title "[Discovery] <title>" --label "type:discovery,priority
|
|
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
|
|
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
|
|
32
|
-
| High | `priority
|
|
33
|
-
| Medium | `priority
|
|
34
|
-
| Low | `priority
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
150
|
+
**Labels for auto-created issues:** `type:discovery`, `area:skills`, `priority::high` (High/Critical severity) or `priority::medium` (Medium severity), `status:ready`.
|