session-orchestrator 3.17.0 → 3.20.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 +185 -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/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +9 -4
- 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 +794 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +245 -50
- package/docs/session-config-template.md +39 -26
- package/hooks/_lib/guard-source-loader.mjs +680 -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 +226 -19
- package/hooks/enforce-scope.mjs +133 -9
- 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 +30 -4
- 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 +616 -164
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +6 -3
- 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/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/emit-session.mjs +3 -40
- 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 +1273 -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/hardening.mjs +9 -9
- 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/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/learnings/select.mjs +550 -0
- 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 +128 -24
- package/scripts/lib/reconcile/engine.mjs +156 -54
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +95 -1
- 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 +568 -145
- package/scripts/lib/session-close-backfill.mjs +63 -8
- 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-record-repair.mjs +551 -0
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- 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/soul-resolve.mjs +12 -0
- 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/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
- package/scripts/lib/validate/check-rules.mjs +244 -36
- 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/check-unwired-features.mjs +549 -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 +218 -16
- package/scripts/print-learnings-index.mjs +474 -0
- package/scripts/release.mjs +534 -0
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +21 -0
- 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/brainstorm/soul.md +47 -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 +117 -18
- package/skills/gitlab-ops/SKILL.md +25 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/grill/soul.md +44 -1
- 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/plan/soul.md +46 -3
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -36
- 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 +32 -2
- package/skills/session-end/plan-verification.md +6 -7
- package/skills/session-end/session-metrics-write.md +2 -0
- 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 +75 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/session-start/soul.md +41 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +6 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +198 -80
- package/templates/_shared/loop.md +4 -4
|
@@ -1,21 +1,78 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// check-rules.mjs — Validate .claude/rules/*.md files
|
|
3
|
-
// against the never-always-on invariant (FA4 #697 backstop for the reconcile emitter
|
|
4
|
-
// brandmauer in scripts/lib/reconcile/emitter.mjs ~line 193).
|
|
2
|
+
// check-rules.mjs — Validate .claude/rules/*.md files against two invariants:
|
|
5
3
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// (1) AUTO-GENERATED rules (`auto-generated: true` — FA4 #697 backstop for the
|
|
5
|
+
// reconcile emitter brandmauer in scripts/lib/reconcile/emitter.mjs ~line
|
|
6
|
+
// 193). HARD-FAIL, exit-code-driving:
|
|
7
|
+
// (a) never-always-on: must have at least one activation axis — a
|
|
8
|
+
// non-empty `globs` array OR a `host-class` key. A rule that is
|
|
9
|
+
// always-on (globs null/absent AND no host-class) is a budget
|
|
10
|
+
// violation (#668 / #687).
|
|
11
|
+
// SPECIAL CASE (#892 QA Defect — the auto-generated twin of #880 QA
|
|
12
|
+
// Defect 1) — an EMPTY `globs` array (`globs: []`) is NOT the
|
|
13
|
+
// "no axis" case and is NOT always-on: rule-loader.mjs
|
|
14
|
+
// unconditionally excludes on `globs.length === 0` (~line 526)
|
|
15
|
+
// AFTER gating runs, so the rule never loads in ANY context — even
|
|
16
|
+
// one where a co-present `host-class:` key would otherwise satisfy
|
|
17
|
+
// the axis. Before #892 this branch silently mis-reported (or, when
|
|
18
|
+
// paired with `host-class:`, silently missed entirely) this case
|
|
19
|
+
// under the inverted "always-on" FAIL message. This gets its own
|
|
20
|
+
// distinct FAIL, fired independent of `host-class`, because
|
|
21
|
+
// `host-class` cannot rescue an empty globs array from that
|
|
22
|
+
// unconditional exclusion — mirrors the handwritten branch's
|
|
23
|
+
// identical `hasEmptyGlobs` fix below.
|
|
24
|
+
// (b) learning-key must be present (traceability back to the emitter source).
|
|
25
|
+
// (c) expires-at must be present (auto-generated rules must have a TTL).
|
|
26
|
+
// Plus, ahead of the auto/handwritten split and binding on EVERY rule file
|
|
27
|
+
// regardless of cohort:
|
|
28
|
+
// (d) the frontmatter must PARSE (#1015). An unparseable file used to be
|
|
29
|
+
// skipped as "not auditable"; rule-loader.mjs, however, treats a
|
|
30
|
+
// parse error as always-on with empty meta, so the skipped file is
|
|
31
|
+
// exactly the one that loads everywhere and clears every gate. See
|
|
32
|
+
// the inline rationale at the parse site below.
|
|
8
33
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
34
|
+
// (2) HANDWRITTEN rules (no `auto-generated: true` — #880 FA5, WARN-only,
|
|
35
|
+
// NEVER affects the exit code). The auto-generated brandmauer above only
|
|
36
|
+
// binds the MACHINE author (the reconcile emitter) — a human authoring a
|
|
37
|
+
// rule file by hand bypasses it entirely. #880's finding: several
|
|
38
|
+
// handwritten rules in this repo carry only a `tier:` axis (no
|
|
39
|
+
// `globs`/`paths`/`host-class`) and no periodic-review marker, and nothing
|
|
40
|
+
// in the system ever prompts a re-review. This second, symmetric check
|
|
41
|
+
// extends the SAME invariant shape to handwritten rules, starting in WARN
|
|
42
|
+
// mode — a hard gate is a later, deliberate step (see
|
|
43
|
+
// docs/rule-authoring.md § "Handwritten Rule Review Date (#880 FA5)").
|
|
44
|
+
// A handwritten rule is flagged when it is missing EITHER:
|
|
45
|
+
// (a) an activation axis — a non-empty `globs`/`paths` array, a
|
|
46
|
+
// `host-class` key, OR a `tier` key. `tier:` counts: rule-loader.mjs
|
|
47
|
+
// `applyGates()` honours it as a real load-context gate (excludes
|
|
48
|
+
// `coordinator-only` rules from wave context and vice versa) — see
|
|
49
|
+
// docs/rule-authoring.md § "Tier gating (issue #692)".
|
|
50
|
+
// SPECIAL CASE — an EMPTY `globs`/`paths` array (`globs: []`) is
|
|
51
|
+
// NOT the "no axis" case and is NOT always-on: rule-loader.mjs
|
|
52
|
+
// unconditionally excludes on `globs.length === 0` AFTER gating
|
|
53
|
+
// runs, so the rule never loads in any context — even one where a
|
|
54
|
+
// co-present `tier:`/`host-class:` key would otherwise satisfy the
|
|
55
|
+
// axis. This branch gets its own distinct WARN, fired independent
|
|
56
|
+
// of `tier`/`host-class`, because neither can rescue an empty
|
|
57
|
+
// globs array from that unconditional exclusion (#880 QA Defect 1
|
|
58
|
+
// fix — see the FAIL/WARN inversion this replaced in git blame).
|
|
59
|
+
// (b) a `review-date` key (ISO 8601 date) — a periodic-review marker,
|
|
60
|
+
// DELIBERATELY DISTINCT from `expires-at`. rule-loader.mjs treats
|
|
61
|
+
// `expires-at` as a live EXPIRY gate (`applyGates`): reusing it on a
|
|
62
|
+
// hand-authored always-on safety rule (e.g. security.md) would make
|
|
63
|
+
// that rule silently stop loading the day the date passes — an
|
|
64
|
+
// unacceptable behaviour change for a metadata-only review marker.
|
|
65
|
+
// `review-date` is inert: it is NOT in rule-loader.mjs's
|
|
66
|
+
// SCALAR_META_KEYS allowlist (contract-locked, not modified by
|
|
67
|
+
// #880), so `parseGlobsFrontmatter()` never surfaces it and
|
|
68
|
+
// `loadApplicableRules()` never gates on it — it is parsed only by
|
|
69
|
+
// THIS script, via the local `hasFrontmatterKey()` helper below.
|
|
15
70
|
//
|
|
16
71
|
// Usage: check-rules.mjs <plugin-root>
|
|
17
|
-
// Outputs lines of the form " PASS: ..." / " FAIL: ..."
|
|
18
|
-
// Exit 0 = all invariants satisfied (
|
|
72
|
+
// Outputs lines of the form " PASS: ..." / " FAIL: ..." / " WARN: ...".
|
|
73
|
+
// Exit 0 = all AUTO-GENERATED invariants satisfied (WARN lines from the
|
|
74
|
+
// handwritten check NEVER affect the exit code); exit 1 = at least one
|
|
75
|
+
// auto-generated FAIL.
|
|
19
76
|
|
|
20
77
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
21
78
|
import { join } from 'node:path';
|
|
@@ -32,6 +89,7 @@ const RULES_DIR = join(pluginRoot, '.claude', 'rules');
|
|
|
32
89
|
|
|
33
90
|
let passed = 0;
|
|
34
91
|
let failed = 0;
|
|
92
|
+
let warned = 0;
|
|
35
93
|
|
|
36
94
|
function pass(msg) {
|
|
37
95
|
console.log(` PASS: ${msg}`);
|
|
@@ -43,8 +101,39 @@ function fail(msg) {
|
|
|
43
101
|
failed++;
|
|
44
102
|
}
|
|
45
103
|
|
|
104
|
+
function warn(msg) {
|
|
105
|
+
console.log(` WARN: ${msg}`);
|
|
106
|
+
warned++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Local frontmatter helper for the `review-date` key (#880 FA5). Deliberately
|
|
111
|
+
// NOT routed through rule-loader.mjs's `parseGlobsFrontmatter()` — that
|
|
112
|
+
// module is contract-locked and its SCALAR_META_KEYS allowlist does not (and
|
|
113
|
+
// should not) recognise `review-date`, per the module-doc rationale above.
|
|
114
|
+
// This is a minimal, self-contained frontmatter-block scan mirroring
|
|
115
|
+
// rule-loader.mjs's own FRONTMATTER_RE, not a general YAML parser.
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns true when the frontmatter block of `contents` contains a top-level
|
|
121
|
+
* `key:` line carrying a non-empty value.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} contents - raw file contents
|
|
124
|
+
* @param {string} key - frontmatter key name (no colon)
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function hasFrontmatterKey(contents, key) {
|
|
128
|
+
const match = FRONTMATTER_RE.exec(contents);
|
|
129
|
+
if (!match) return false;
|
|
130
|
+
const keyRe = new RegExp(`^${key}:\\s*(\\S.*)?$`, 'm');
|
|
131
|
+
const keyMatch = keyRe.exec(match[1]);
|
|
132
|
+
return Boolean(keyMatch && keyMatch[1] && keyMatch[1].trim() !== '');
|
|
133
|
+
}
|
|
134
|
+
|
|
46
135
|
// ============================================================================
|
|
47
|
-
// Check:
|
|
136
|
+
// Check: .claude/rules/ rule invariants
|
|
48
137
|
// ============================================================================
|
|
49
138
|
console.log('--- Check: .claude/rules/ auto-generated rule invariants ---');
|
|
50
139
|
|
|
@@ -52,7 +141,7 @@ console.log('--- Check: .claude/rules/ auto-generated rule invariants ---');
|
|
|
52
141
|
if (!existsSync(RULES_DIR)) {
|
|
53
142
|
pass('no .claude/rules/ directory found — nothing to validate');
|
|
54
143
|
console.log('');
|
|
55
|
-
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
144
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${warned} warned`);
|
|
56
145
|
process.exit(0);
|
|
57
146
|
}
|
|
58
147
|
|
|
@@ -67,12 +156,16 @@ try {
|
|
|
67
156
|
if (mdFiles.length === 0) {
|
|
68
157
|
pass('no .md rule files found — nothing to validate');
|
|
69
158
|
console.log('');
|
|
70
|
-
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
159
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${warned} warned`);
|
|
71
160
|
process.exit(0);
|
|
72
161
|
}
|
|
73
162
|
|
|
74
|
-
//
|
|
75
|
-
|
|
163
|
+
// Parse every rule file once, splitting into the auto-generated and
|
|
164
|
+
// handwritten cohorts. Malformed frontmatter → a cohort-independent hard FAIL
|
|
165
|
+
// (#1015). It was previously skipped from BOTH checks ("neither PASS'd nor
|
|
166
|
+
// FAIL'd"); see the parse site below for why that abstention was a blind spot.
|
|
167
|
+
const autoGeneratedEntries = [];
|
|
168
|
+
const handwrittenEntries = [];
|
|
76
169
|
|
|
77
170
|
for (const name of mdFiles.sort()) {
|
|
78
171
|
const filePath = join(RULES_DIR, name);
|
|
@@ -87,29 +180,72 @@ for (const name of mdFiles.sort()) {
|
|
|
87
180
|
let parsed;
|
|
88
181
|
try {
|
|
89
182
|
parsed = parseGlobsFrontmatter(contents);
|
|
90
|
-
} catch {
|
|
91
|
-
//
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// MALFORMED FRONTMATTER IS A HARD FAIL (#1015) — it used to `continue`.
|
|
185
|
+
//
|
|
186
|
+
// The skip looked like a neutral abstention ("not auditable by either
|
|
187
|
+
// branch") but was in fact this validator's single blind spot, and it was
|
|
188
|
+
// blind to precisely the worst state. rule-loader.mjs catches the SAME
|
|
189
|
+
// throw (~:500-507), falls back to `globs = null, meta = {}, parseError =
|
|
190
|
+
// true`, and then (~:519-530) pushes the entry with `alwaysOn: true`.
|
|
191
|
+
// Empty meta means applyGates() has nothing to gate on, so the file also
|
|
192
|
+
// clears tier/host-class/mode/EXPIRY gating by design ("a rule is never
|
|
193
|
+
// silently dropped"). Net effect: the one file this branch declined to
|
|
194
|
+
// audit is the one file the loader loads ALWAYS-ON, in every context,
|
|
195
|
+
// forever — the exact state the never-always-on invariant (#668/#687)
|
|
196
|
+
// exists to forbid, and the landing state of a frontmatter-injection whose
|
|
197
|
+
// payload happens to be colon-less (an injected `\n` + a line with no `:`).
|
|
198
|
+
// Unparseable therefore means UNSAFE, not "unknown": FAIL, never skip.
|
|
199
|
+
fail(
|
|
200
|
+
`.claude/rules/${name} — frontmatter does not parse (${err.message}) — rule-loader.mjs treats a parse ` +
|
|
201
|
+
'error as ALWAYS-ON with EMPTY meta, so this file loads in every context and clears every gate ' +
|
|
202
|
+
'(no expiry, no tier, no host-class, no mode). Fix the frontmatter or remove the file.',
|
|
203
|
+
);
|
|
92
204
|
continue;
|
|
93
205
|
}
|
|
94
206
|
|
|
95
207
|
const { globs, meta } = parsed;
|
|
96
|
-
|
|
97
|
-
// Only validate rules that explicitly declare auto-generated: true.
|
|
98
|
-
if (meta['auto-generated'] !== true) continue;
|
|
99
|
-
|
|
100
|
-
autoGeneratedCount++;
|
|
101
208
|
const rel = `.claude/rules/${name}`;
|
|
102
209
|
|
|
210
|
+
if (meta['auto-generated'] === true) {
|
|
211
|
+
autoGeneratedEntries.push({ rel, globs, meta });
|
|
212
|
+
} else {
|
|
213
|
+
handwrittenEntries.push({ rel, globs, meta, contents });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Auto-generated branch (FA4 #697) — UNCHANGED behaviour, hard-fail.
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
for (const { rel, globs, meta } of autoGeneratedEntries) {
|
|
103
221
|
// (a) never-always-on: must have at least one activation axis.
|
|
104
|
-
// globs is null (no frontmatter / no globs key)
|
|
105
|
-
//
|
|
222
|
+
// globs is one of: null (no frontmatter / no globs key), [] (key present
|
|
223
|
+
// but EMPTY — the #892 QA-Defect special case, see module doc), or a
|
|
224
|
+
// populated array. host-class absent → no host-class axis.
|
|
225
|
+
const hasEmptyGlobs = Array.isArray(globs) && globs.length === 0;
|
|
106
226
|
const hasGlobs = Array.isArray(globs) && globs.length > 0;
|
|
107
227
|
const hasHostClass = Object.prototype.hasOwnProperty.call(meta, 'host-class');
|
|
108
228
|
|
|
109
|
-
if (
|
|
229
|
+
if (hasEmptyGlobs) {
|
|
230
|
+
// An empty globs array is NOT "no axis at all" and is NOT "always-on" —
|
|
231
|
+
// it is the opposite: rule-loader.mjs's unconditional globs.length === 0
|
|
232
|
+
// exclusion (~line 526) fires AFTER gating, so this rule never loads in
|
|
233
|
+
// ANY context, even one that also carries a host-class: key. Fire
|
|
234
|
+
// independent of hasHostClass — host-class cannot rescue an empty globs
|
|
235
|
+
// array from that unconditional exclusion (#892 QA Defect fix — see the
|
|
236
|
+
// module doc for the full rule-loader.mjs citation and the identical
|
|
237
|
+
// handwritten-branch fix this mirrors, #880 QA Defect 1).
|
|
238
|
+
fail(
|
|
239
|
+
`${rel} — auto-generated rule has an empty globs array (globs: []) — this rule matches NOTHING and never ` +
|
|
240
|
+
'loads in ANY context, even one that also carries a host-class: key (rule-loader.mjs excludes on ' +
|
|
241
|
+
'globs.length === 0 unconditionally, after gating runs). This is the OPPOSITE of "always-on" but still ' +
|
|
242
|
+
'violates the never-always-on invariant (#668/#687) because the rule never activates at all. Populate ' +
|
|
243
|
+
'globs: with real patterns or remove the file.',
|
|
244
|
+
);
|
|
245
|
+
} else if (!hasGlobs && !hasHostClass) {
|
|
110
246
|
fail(
|
|
111
|
-
`${rel} — auto-generated rule is always-on (no activation axis: globs is absent
|
|
112
|
-
|
|
247
|
+
`${rel} — auto-generated rule is always-on (no activation axis: globs is absent AND host-class absent). ` +
|
|
248
|
+
'This violates the never-always-on invariant (#668/#687). Add a globs filter or host-class axis.',
|
|
113
249
|
);
|
|
114
250
|
}
|
|
115
251
|
|
|
@@ -123,21 +259,93 @@ for (const name of mdFiles.sort()) {
|
|
|
123
259
|
fail(`${rel} — auto-generated rule is missing required frontmatter key: expires-at`);
|
|
124
260
|
}
|
|
125
261
|
|
|
126
|
-
// Emit a PASS line only when the rule satisfies ALL three invariants:
|
|
127
|
-
//
|
|
128
|
-
//
|
|
262
|
+
// Emit a PASS line only when the rule satisfies ALL three invariants: a
|
|
263
|
+
// genuine activation axis (a NON-EMPTY globs array, or host-class — NEVER
|
|
264
|
+
// an empty globs array, which is dead-not-passing regardless of host-class)
|
|
265
|
+
// AND learning-key AND expires-at. When any failed, the FAIL line(s) above
|
|
266
|
+
// already emitted.
|
|
129
267
|
const lkOk = Object.prototype.hasOwnProperty.call(meta, 'learning-key');
|
|
130
268
|
const eaOk = Object.prototype.hasOwnProperty.call(meta, 'expires-at');
|
|
131
|
-
if ((hasGlobs || hasHostClass) && lkOk && eaOk) {
|
|
269
|
+
if (!hasEmptyGlobs && (hasGlobs || hasHostClass) && lkOk && eaOk) {
|
|
132
270
|
pass(`${rel} — auto-generated rule satisfies all invariants`);
|
|
133
271
|
}
|
|
134
272
|
}
|
|
135
273
|
|
|
136
|
-
if (
|
|
274
|
+
if (autoGeneratedEntries.length === 0) {
|
|
137
275
|
pass('no auto-generated rules found — nothing to validate');
|
|
138
276
|
}
|
|
139
277
|
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Handwritten branch (#880 FA5) — WARN-only, non-fatal, never affects exit code.
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(
|
|
283
|
+
'--- Check: .claude/rules/ handwritten-rule activation + review-date (warn mode, #880) ---',
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
for (const { rel, globs, meta, contents } of handwrittenEntries) {
|
|
287
|
+
// `globs` (the merged globs/paths value from parseGlobsFrontmatter — #795
|
|
288
|
+
// alias, `globs:` wins when both are present) is one of:
|
|
289
|
+
// - null → neither key present
|
|
290
|
+
// - [] (Array, length 0) → key present but EMPTY
|
|
291
|
+
// - [...] (Array, length>0) → key present and populated
|
|
292
|
+
const hasEmptyGlobs = Array.isArray(globs) && globs.length === 0;
|
|
293
|
+
const hasGlobsAxis = Array.isArray(globs) && globs.length > 0;
|
|
294
|
+
const hasHostClassAxis = Object.prototype.hasOwnProperty.call(meta, 'host-class');
|
|
295
|
+
const hasTierAxis = Object.prototype.hasOwnProperty.call(meta, 'tier');
|
|
296
|
+
const hasAxis = hasGlobsAxis || hasHostClassAxis || hasTierAxis;
|
|
297
|
+
const hasReviewDate = hasFrontmatterKey(contents, 'review-date');
|
|
298
|
+
|
|
299
|
+
if (hasEmptyGlobs) {
|
|
300
|
+
// An empty globs/paths array is NOT the same condition as "no axis at
|
|
301
|
+
// all", and it is NOT "always-on" — it is the opposite. Per
|
|
302
|
+
// rule-loader.mjs's loadApplicableRules() (~line 526): once frontmatter
|
|
303
|
+
// parses, tier/host-class/mode/expiry gating (applyGates) runs FIRST and
|
|
304
|
+
// can only ADD exclusions; if the rule survives that gating, the loader
|
|
305
|
+
// THEN unconditionally excludes it when `globs.length === 0` ("matches
|
|
306
|
+
// nothing (intentionally scoped out)") — this check fires regardless of
|
|
307
|
+
// whether a tier:/host-class: key would otherwise have satisfied the
|
|
308
|
+
// axis requirement. So a rule with `globs: []` never loads in ANY
|
|
309
|
+
// context, even one paired with `tier: always`. Always warn with the
|
|
310
|
+
// accurate "dead rule" message here, independent of `hasAxis` — a
|
|
311
|
+
// co-present tier:/host-class: key cannot rescue an empty globs array
|
|
312
|
+
// from this unconditional exclusion, so it must not suppress the warning
|
|
313
|
+
// either (#880 QA Defect 1 fix).
|
|
314
|
+
warn(
|
|
315
|
+
`${rel} — handwritten rule has an empty globs/paths array (globs: [] or paths: []) — this rule matches ` +
|
|
316
|
+
'NOTHING and never loads in ANY context, even one that also carries a tier:/host-class: key ' +
|
|
317
|
+
'(rule-loader.mjs excludes on globs.length === 0 unconditionally, after gating runs). This is the ' +
|
|
318
|
+
'OPPOSITE of "always-on". If this is a deliberately disabled rule, consider removing the file instead; ' +
|
|
319
|
+
'otherwise populate globs:/paths: with real patterns.',
|
|
320
|
+
);
|
|
321
|
+
} else if (!hasAxis) {
|
|
322
|
+
warn(
|
|
323
|
+
`${rel} — handwritten rule has no activation axis (globs/paths/host-class/tier all absent) — loads ` +
|
|
324
|
+
'always-on in every context. Not a build failure yet (warn mode, #880); add a tier: or globs: axis ' +
|
|
325
|
+
'if this rule should be scoped.',
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!hasReviewDate) {
|
|
330
|
+
warn(
|
|
331
|
+
`${rel} — handwritten rule is missing a review-date (no periodic-review marker). ` +
|
|
332
|
+
'See docs/rule-authoring.md § "Handwritten Rule Review Date (#880 FA5)".',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!hasEmptyGlobs && hasAxis && hasReviewDate) {
|
|
337
|
+
pass(`${rel} — handwritten rule has an activation axis and a review-date`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (handwrittenEntries.length === 0) {
|
|
342
|
+
// Deliberately NOT routed through pass() — this is bookkeeping, not an
|
|
343
|
+
// audited invariant, and must not inflate the `passed` count that
|
|
344
|
+
// auto-generated-only fixtures assert an exact value against.
|
|
345
|
+
console.log(' (no handwritten rules found — nothing to check)');
|
|
346
|
+
}
|
|
347
|
+
|
|
140
348
|
console.log('');
|
|
141
|
-
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
349
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${warned} warned`);
|
|
142
350
|
|
|
143
351
|
process.exit(failed > 0 ? 1 : 0);
|