session-orchestrator 4.2.0 → 5.0.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 (129) hide show
  1. package/.agents/skills/session-start/SKILL.md +1 -1
  2. package/.agents/skills/ux-grill/SKILL.md +22 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +3 -2
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  7. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  8. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  9. package/.cursor/commands/ux-grill.md +14 -0
  10. package/.cursor/skills/session-start/SKILL.md +1 -1
  11. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  12. package/.cursor-plugin/plugin.json +1 -1
  13. package/AGENTS.md +2 -1
  14. package/CHANGELOG.md +94 -1
  15. package/README.md +98 -86
  16. package/agents/dialectic-deriver.md +11 -0
  17. package/commands/ux-grill.md +51 -0
  18. package/docs/USER-GUIDE.md +2 -2
  19. package/docs/codex-setup.md +8 -0
  20. package/docs/components.md +7 -7
  21. package/docs/events-schema.md +9 -5
  22. package/docs/github-mirror-protection.md +61 -20
  23. package/docs/migration-v5.md +62 -0
  24. package/docs/scope-collision-guard.md +14 -0
  25. package/hooks/_lib/hook-import-set.json +44 -2
  26. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  27. package/hooks/_lib/vcs-create-matcher.mjs +190 -3
  28. package/hooks/enforce-scope.mjs +201 -0
  29. package/hooks/hooks-codex.json +1 -1
  30. package/hooks/hooks-cursor.json +5 -0
  31. package/hooks/hooks.json +7 -2
  32. package/hooks/on-session-start.mjs +171 -49
  33. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  34. package/hooks/pre-auq-clarity.mjs +70 -18
  35. package/hooks/pre-bash-issue-budget.mjs +51 -4
  36. package/package.json +2 -1
  37. package/pi/prompts/ux-grill.md +12 -0
  38. package/scripts/ci/assert-vitest-green.mjs +4 -2
  39. package/scripts/dialectic-deriver.mjs +32 -8
  40. package/scripts/emit-session.mjs +72 -1
  41. package/scripts/lib/agent-status.mjs +441 -9
  42. package/scripts/lib/auq/schema.mjs +10 -3
  43. package/scripts/lib/ci-status-banner.mjs +29 -6
  44. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  45. package/scripts/lib/config.mjs +12 -1
  46. package/scripts/lib/eval/engine.mjs +7 -1
  47. package/scripts/lib/file-lock.mjs +114 -13
  48. package/scripts/lib/git-porcelain.mjs +113 -0
  49. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  50. package/scripts/lib/io.mjs +29 -4
  51. package/scripts/lib/issue-budget.mjs +336 -6
  52. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  53. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  54. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  55. package/scripts/lib/maintenance-due-banner.mjs +11 -1
  56. package/scripts/lib/peer-cards/merger.mjs +143 -0
  57. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  58. package/scripts/lib/project-hygiene.mjs +81 -30
  59. package/scripts/lib/quality-gate.mjs +14 -65
  60. package/scripts/lib/reconcile/engine.mjs +19 -1
  61. package/scripts/lib/reconcile/writer.mjs +278 -11
  62. package/scripts/lib/scope-echo.mjs +346 -0
  63. package/scripts/lib/session-lock.mjs +62 -2
  64. package/scripts/lib/session-record-repair.mjs +91 -0
  65. package/scripts/lib/session-schema/filters.mjs +26 -1
  66. package/scripts/lib/session-start-probes.mjs +419 -53
  67. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  68. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  69. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  70. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  71. package/scripts/lib/ux-grill/compare.mjs +285 -0
  72. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  73. package/scripts/lib/ux-grill/measures.mjs +431 -0
  74. package/scripts/lib/ux-grill/paths.mjs +224 -0
  75. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  76. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  77. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  78. package/scripts/lib/ux-grill/schema.mjs +321 -0
  79. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  80. package/scripts/lib/validate/check-unwired-features.mjs +48 -20
  81. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  82. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  83. package/scripts/mcp-server.sh +16 -1
  84. package/scripts/release.mjs +7 -2
  85. package/skills/bootstrap/SKILL.md +12 -209
  86. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  87. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  88. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  89. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  90. package/skills/claude-md-drift-check/SKILL.md +9 -2
  91. package/skills/claude-md-drift-check/checker.mjs +213 -21
  92. package/skills/discovery/SKILL.md +6 -173
  93. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  94. package/skills/discovery/probes-docs.md +8 -4
  95. package/skills/discovery/probes-supply-chain.md +4 -2
  96. package/skills/discovery/probes-ui.md +7 -3
  97. package/skills/discovery/probes-vault.md +12 -4
  98. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  99. package/skills/discovery/references/discovery-triage-state.md +54 -0
  100. package/skills/eval/rubric-v1.md +13 -0
  101. package/skills/evolve/SKILL.md +2 -458
  102. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  103. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  104. package/skills/plan/mode-retro.md +4 -3
  105. package/skills/reconcile/SKILL.md +10 -0
  106. package/skills/session-end/drift-operations.md +20 -5
  107. package/skills/session-end/metrics-collection.md +1 -0
  108. package/skills/session-end/phase-3-6-tail.md +4 -2
  109. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  110. package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
  111. package/skills/session-end/session-metrics-write.md +2 -0
  112. package/skills/session-plan/SKILL.md +2 -144
  113. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  114. package/skills/session-start/SKILL.md +24 -6
  115. package/skills/session-start/references/operations-contract.md +114 -0
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
  117. package/skills/session-start/soul.md +2 -2
  118. package/skills/test-runner/SKILL.md +1 -1
  119. package/skills/tmux-layout/SKILL.md +3 -1
  120. package/skills/ux-grill/SKILL.md +211 -0
  121. package/skills/ux-grill/rubric-v2.md +201 -0
  122. package/skills/ux-grill/soul.md +76 -0
  123. package/skills/wave-executor/SKILL.md +3 -128
  124. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  125. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  126. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  127. package/skills/wave-executor/references/wave-loop-review.md +18 -5
  128. package/templates/_shared/ux-manifest.template.md +149 -0
  129. package/templates/_shared/journey-manifest.md +0 -114
@@ -0,0 +1,108 @@
1
+ # Bootstrap — Retroactive Flow (`--retroactive`)
2
+
3
+ > Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
5
+
6
+ ## Retroactive Flow (`--retroactive`)
7
+
8
+ Entered when `$ARGUMENTS` contains `--retroactive`. Writes the lock file and, per #182, optionally patches missing mandatory Session Config fields with defaults.
9
+
10
+ **Purpose:** Adopt an existing repo that already has `CLAUDE.md` + `## Session Config` but was bootstrapped manually (no `bootstrap.lock`). Writes the lock so the gate passes on all future invocations, and ensures the Session Config block satisfies the validated schema defined in `scripts/lib/config-schema.mjs`.
11
+
12
+ **Steps:**
13
+
14
+ 1. **Verify preconditions.** Confirm `CLAUDE.md` (or `AGENTS.md`) exists and contains `## Session Config`. If not, abort: `Error: CLAUDE.md with Session Config required for retroactive bootstrap.`
15
+
16
+ 2. **Check lock not already present.** If `.orchestrator/bootstrap.lock` already exists and has valid `version` + `tier` fields, report: `bootstrap.lock already present (tier: <tier>). Nothing to do.` and exit 0 (idempotent).
17
+
18
+ 3. **Infer tier from file inventory.** Examine the repo root:
19
+
20
+ | Condition (evaluated in order) | Inferred Tier |
21
+ |---|---|
22
+ | CI file present (`.gitlab-ci.yml` OR `.github/workflows/`) AND `CHANGELOG.md` present | `deep` |
23
+ | Package manifest present (`package.json` OR `pyproject.toml`) | `standard` |
24
+ | Neither of the above | `fast` |
25
+
26
+ Store as `INFERRED_TIER`.
27
+
28
+ 4. **Infer archetype.** Run Phase 0.5's read-only source detection. For a private
29
+ contract, use its detected `selected.id`; retain `null` with an explicit
30
+ `insufficient-evidence` report if no markers match. An invalid configured
31
+ contract aborts. Do not scaffold or apply rules in this retroactive flow.
32
+ For the public path, use best-effort detection from existing files:
33
+ - `pyproject.toml` present → `python-uv`
34
+ - `package.json` with `next` in dependencies → `nextjs-minimal`
35
+ - `package.json` without `next` → `node-minimal`
36
+ - No manifest → `null`
37
+
38
+ Store as `INFERRED_ARCHETYPE`.
39
+
40
+ 5. **Write bootstrap.lock.** Create `.orchestrator/` if needed, then write:
41
+ ```yaml
42
+ # .orchestrator/bootstrap.lock
43
+ version: 1
44
+ tier: <INFERRED_TIER>
45
+ archetype: <INFERRED_ARCHETYPE or null>
46
+ timestamp: <current ISO 8601 UTC>
47
+ source: retroactive
48
+ plugin-version: <current plugin version from $PLUGIN_ROOT/package.json>
49
+ ```
50
+
51
+ 6. **Patch Session Config (#182).** Run the validator against the current `## Session Config` block; append any missing mandatory fields with defaults. The 7 mandatory fields (per `scripts/lib/config-schema.mjs`) are: `test-command`, `typecheck-command`, `lint-command`, `agents-per-wave`, `waves`, `persistence`, `enforcement`.
52
+
53
+ ```bash
54
+ CONFIG_OUT="$(node "$PLUGIN_ROOT/scripts/parse-config.mjs" 2>&1 >/dev/null)"
55
+ # parse-config.mjs emits validation warnings to stderr when enforcement=warn.
56
+ # Grep for 'must be' lines (issued by validate-config.mjs) to detect missing fields.
57
+ MISSING_FIELDS="$(echo "$CONFIG_OUT" | grep -oE '(test-command|typecheck-command|lint-command|agents-per-wave|waves|persistence|enforcement)' | sort -u || true)"
58
+ if [[ -n "$MISSING_FIELDS" ]]; then
59
+ # Detect package manager to pick sensible defaults for commands.
60
+ PM_DEFAULTS="$(node --input-type=module -e "
61
+ import {detectPackageManager, defaultQualityGateCommands} from '$PLUGIN_ROOT/scripts/lib/package-manager.mjs';
62
+ const pm = detectPackageManager(process.cwd());
63
+ const cmds = defaultQualityGateCommands(pm);
64
+ console.log('test-command: ' + cmds.test.command);
65
+ console.log('typecheck-command: ' + cmds.typecheck.command);
66
+ console.log('lint-command: ' + cmds.lint.command);
67
+ " 2>/dev/null)"
68
+
69
+ CONFIG_FILE="CLAUDE.md"
70
+ [[ -f "AGENTS.md" ]] && CONFIG_FILE="AGENTS.md"
71
+
72
+ # Append each missing field under the ## Session Config block.
73
+ for field in $MISSING_FIELDS; do
74
+ case "$field" in
75
+ test-command|typecheck-command|lint-command)
76
+ default_line="$(echo "$PM_DEFAULTS" | grep "^$field:")" ;;
77
+ agents-per-wave) default_line="agents-per-wave: 6" ;;
78
+ waves) default_line="waves: 5" ;;
79
+ persistence) default_line="persistence: true" ;;
80
+ enforcement) default_line="enforcement: warn" ;;
81
+ esac
82
+ # Insert after `## Session Config` line if not already present.
83
+ grep -q "^$field:" "$CONFIG_FILE" \
84
+ || awk -v insert="$default_line" '/^## Session Config/ && !done { print; print ""; print insert; done=1; next } { print }' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" \
85
+ && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
86
+ done
87
+ echo "Patched $CONFIG_FILE with defaults for: $MISSING_FIELDS"
88
+ fi
89
+ ```
90
+
91
+ This patch is best-effort: existing fields are never overwritten. If no fields are missing, this step is a no-op.
92
+
93
+ 7. **Commit.** Stage the lock file (and the patched config file, if it changed) and commit:
94
+ ```bash
95
+ mkdir -p .orchestrator
96
+ git add .orchestrator/bootstrap.lock
97
+ # Also stage CLAUDE.md/AGENTS.md if step 6 patched it.
98
+ git diff --name-only --cached CLAUDE.md AGENTS.md 2>/dev/null | head -1 >/dev/null || {
99
+ [[ -f CLAUDE.md ]] && git diff --quiet CLAUDE.md || git add CLAUDE.md
100
+ [[ -f AGENTS.md ]] && git diff --quiet AGENTS.md || git add AGENTS.md
101
+ }
102
+ git commit -m "chore: bootstrap lock (retroactive)"
103
+ ```
104
+
105
+ 8. **Report.** Print: `Retroactive bootstrap complete. Lock written (tier: <INFERRED_TIER>, source: retroactive).` Include a second line `Patched Session Config: <fields>` when step 6 applied any patches, otherwise `No config changes.`.
106
+
107
+ ---
108
+
@@ -0,0 +1,64 @@
1
+ # Bootstrap — Phase 3.6: (Optional) Rules-Fetch Bridge
2
+
3
+ > Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`, `private-contract.md` → `../private-contract.md`, `standard-template.md` → `../standard-template.md`, `deep-template.md` → `../deep-template.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
5
+
6
+ ## Phase 3.6: (Optional) Rules-Fetch Bridge
7
+
8
+ > Closes session-orchestrator issue #110.
9
+
10
+ After scaffolding, the Standard and Deep templates execute S99. On the private
11
+ path, it applies the selected contract's local rule union, rechecks conditional
12
+ dependencies and preserves existing files. It excludes every plugin-owned
13
+ basename and aborts on an invalid configured contract; see `private-contract.md`.
14
+
15
+ On the public path, S99 retains the optional remote rules-fetch step. It pulls
16
+ canonical `.claude/rules/*.md` directly from the configured baseline GitLab
17
+ project, excluding all plugin-owned basenames. The remote step only fires when:
18
+
19
+ - `baseline-ref` is present in Session Config
20
+ - `GITLAB_TOKEN` env var is set
21
+ - `scripts/lib/fetch-baseline.mjs` is present in the plugin
22
+ - A GitLab host is resolvable from the `gitlab-host` Session Config key (or the `GITLAB_HOST` env var) — never a hardcoded default
23
+
24
+ When triggered, the step:
25
+
26
+ 1. Loops over a default rule manifest, invoking `node scripts/lib/fetch-baseline.mjs <project_id> <file_path> <baseline-ref>` once per rule. The CLI prints one file body to stdout (exit 0 success; 1 auth, 2 not-found, 3 network) — bootstrap redirects stdout to the target path and skips failures so a single 404 cannot abort the batch.
27
+ 2. Fetches each rule listed in the default manifest from the configured `baseline-project-id` (default `52`) at the configured `baseline-ref`
28
+ 3. Writes `.claude/.baseline-fetch.lock` (via an inline `node --input-type=module -e`) recording what was fetched
29
+ 4. Populates `.claude/.baseline-cache/` for offline fallback on subsequent invocations
30
+
31
+ When the fetch fails (network error, auth, missing file), bootstrap **does not abort**. Rules will arrive in the repo via Clank's weekly baseline sync MRs (the legacy path). A warning is printed.
32
+
33
+ **Why opt-in:** Repos without `baseline-ref` continue to receive rules via the existing Clank sync flow. The fetch bridge is a faster on-demand alternative for newly-bootstrapped repos that want current rules immediately.
34
+
35
+ **Local edits:** Re-running bootstrap with `baseline-ref` set will overwrite `.claude/rules/*.md` (rules are canonical). Repo-specific extensions belong in `.claude/rules/local/*.md` (not fetched, not overwritten).
36
+
37
+ See `standard-template.md` (Step S99) and `deep-template.md` (Step D99) for the implementation, and `docs/session-config-reference.md` for the `baseline-ref` and `baseline-project-id` field definitions.
38
+
39
+ ### `.claude/.baseline-fetch.lock` Schema
40
+
41
+ The lock file is committed to git and records what was fetched.
42
+
43
+ ```yaml
44
+ # .claude/.baseline-fetch.lock
45
+ version: 1
46
+ project_id: 52
47
+ baseline_ref: main
48
+ fetched_at: 2026-04-17T13:42:00Z # ISO 8601 UTC
49
+ files:
50
+ - .claude/rules/development.md
51
+ - .claude/rules/security.md
52
+ - .claude/rules/...
53
+ ```
54
+
55
+ | Field | Description |
56
+ |---|---|
57
+ | `version` | Lock file schema version. Currently `1`. |
58
+ | `project_id` | GitLab project ID the files were fetched from. |
59
+ | `baseline_ref` | The git ref (branch/tag/SHA) at fetch time. |
60
+ | `fetched_at` | ISO 8601 UTC timestamp. |
61
+ | `files` | List of fetched file paths (relative to repo root). |
62
+
63
+ ---
64
+
@@ -41,7 +41,7 @@ PHASE 1 IMPLEMENTED (2026-04-19). Session-end opt-in quality gate. Upstream of `
41
41
  | 6 | `session-config-parity` | Top-level keys under `## Session Config` in `CLAUDE.md` / `AGENTS.md` | diff against `docs/session-config-template.md`; a missing MANDATORY (minimal-baseline) key is an error, a missing OPT-IN-baseline-only key is a warning |
42
42
  | 7 | `vault-dir-parity` | `vault-integration.vault-dir` in BOTH `CLAUDE.md` AND `AGENTS.md` | reuse `_parseVaultIntegration`; flag when the two files disagree. Short-circuits to PASS when the two are the SAME file by construction (symlink / same inode / byte-identical generated copy) |
43
43
  | 8 | `generated-rule-staleness` *(WARN only)* | `.claude/rules/*.md` with `auto-generated: true` frontmatter | extract `learning-key`; WARN when the key is absent from `.orchestrator/metrics/learnings.jsonl` or its learning's `expires_at` is in the past; skipped silently when no auto-generated rules exist |
44
- | 9 | `rule-scoping` | `.claude/rules/*.md` frontmatter + `## See Also` footers + `.claude/rules/<name>.md` citations in `CLAUDE.md`/`AGENTS.md` | five probes: `paths:` frontmatter (error), cited-but-missing rule citations (error), zero-match `globs:` patterns (warn), foreign PascalCase glob tokens (warn), unreadable rule files (warn — surfaced instead of silently skipped); skipped silently when `.claude/rules/` is absent |
44
+ | 9 | `rule-scoping` | `.claude/rules/*.md` frontmatter + `## See Also` footers + `.claude/rules/<name>.md` citations in `CLAUDE.md`/`AGENTS.md` | six probes: `paths:` frontmatter (error), cited-but-missing rule citations (error), zero-match `globs:` patterns (warn), foreign PascalCase glob tokens (warn), unreadable rule files (warn — surfaced instead of silently skipped), declared fleet-intent globs (**note**, never warn); skipped silently when `.claude/rules/` is absent |
45
45
  | 10 | `docs-parity` | `docs/components.md` count-claims, Session Config key parity (`docs/session-config-template.md` vs `docs/session-config-reference.md`), `.claude/metrics/` stale-path references in `docs/*.md` / `docs/examples/*.md` | three sub-checks (a/b/c) — see below; skipped silently when `docs/components.md` is absent |
46
46
 
47
47
  Check 3 deliberately scopes to forward-looking sections. Mentions inside "Recently Closed", "Decisions", "Archive", etc. describe history and must not be flagged.
@@ -65,7 +65,9 @@ Check 7 (issue #600) is the **only** check that intentionally reads BOTH instruc
65
65
 
66
66
  The same predicate guards Check 9's probe 2a, which iterates `['CLAUDE.md', 'AGENTS.md']`: with an aliased `AGENTS.md` it scans `CLAUDE.md` only, because otherwise every dangling `.claude/rules/*.md` citation would be reported TWICE — one defect, two errors, and in `--mode strict` a doubled blocking surface against a file the operator is not allowed to edit.
67
67
 
68
- Check 9 (`rule-scoping`) validates `.claude/rules/*.md` frontmatter against the `scripts/lib/rule-loader.mjs` contract, catching the class of defect where a rule silently drifts out of the activation pipeline the loader actually implements. Four probes: **(1) paths-presence** — a top-level `paths:` frontmatter key is not a key `rule-loader.mjs` recognises (it only reads `globs:`), so a rule with `paths:` silently loads ALWAYS-ON regardless of intended file scope; flagged as an error. **(2) cited-but-missing** — `(a)` `.claude/rules/<name>.md` citations inside `CLAUDE.md`/`AGENTS.md` that don't resolve to a file on disk, and `(b)` bare `<name>.md` tokens in a rule's own `## See Also` footer that don't exist as sibling rule files (tokens carrying a path separator, e.g. `../../skills/_shared/state-ownership.md`, are cross-directory references and explicitly out of scope); both flagged as errors. **(3) zero-match-globs** — a `globs:` pattern matching zero files in `git ls-files` (falls back to a manual directory walk when git is unavailable); flagged as a WARNING, not an error, because library/exemplar repos legitimately carry dead stack rules (this repo alone carries ~37 by design — Swift/Next.js/Supabase rules with no matching files in a pure-Node-ESM codebase). **(4) foreign-glob** — a glob pattern containing a PascalCase product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope; flagged as a WARNING. Glob matching reuses the same picomatch-with-inline-fallback resolution `scripts/lib/rule-loader.mjs` uses (`parseGlobsFrontmatter` is imported directly; the picomatch resolution itself is duplicated locally since `rule-loader.mjs` does not export a public matcher function). The check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `.claude/rules/` is absent, or explicitly via `--skip-rule-scoping`.
68
+ Check 9 (`rule-scoping`) validates `.claude/rules/*.md` frontmatter against the `scripts/lib/rule-loader.mjs` contract, catching the class of defect where a rule silently drifts out of the activation pipeline the loader actually implements. Six probes: **(1) paths-presence** — a top-level `paths:` frontmatter key is not a key `rule-loader.mjs` recognises (it only reads `globs:`), so a rule with `paths:` silently loads ALWAYS-ON regardless of intended file scope; flagged as an error. **(2) cited-but-missing** — `(a)` `.claude/rules/<name>.md` citations inside `CLAUDE.md`/`AGENTS.md` that don't resolve to a file on disk, and `(b)` bare `<name>.md` tokens in a rule's own `## See Also` footer that don't exist as sibling rule files (tokens carrying a path separator, e.g. `../../skills/_shared/state-ownership.md`, are cross-directory references and explicitly out of scope); both flagged as errors. **(3) zero-match-globs** — a `globs:` pattern matching zero files in `git ls-files` (falls back to a manual directory walk when git is unavailable); flagged as a WARNING, not an error, because library/exemplar repos legitimately carry dead stack rules. **(4) foreign-glob** — a glob pattern containing a PascalCase product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope; flagged as a WARNING. **(5) unreadable-file** — a rule file that could not be read (permissions, a race with a concurrent delete); flagged as a WARNING rather than silently skipped, so a completeness audit does not drop files, and so an unreadable file cannot brick the gate under `mode: hard`. **(6) fleet-intent-glob** — a `globs:` pattern DECLARED as fleet intent goes to `notes[]` (see below), never to `warnings[]`, and the declaration also settles probe 4 for that same pattern: a pattern the rule author declared is by construction not a copy-paste leftover. Glob matching reuses the same picomatch-with-inline-fallback resolution `scripts/lib/rule-loader.mjs` uses (`parseGlobsFrontmatter` is imported directly; the picomatch resolution itself is duplicated locally since `rule-loader.mjs` does not export a public matcher function). The check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `.claude/rules/` is absent, or explicitly via `--skip-rule-scoping`.
69
+
70
+ **Fleet-intent declaration (Check 9, probe 6).** A rule may declare that a `globs:` pattern is aimed at CONSUMER repos rather than this one, via its own frontmatter key `fleet-intent-globs:`. Accepted forms are block style (`fleet-intent-globs:` plus indented ` - "<pattern>"` lines), flow style (`fleet-intent-globs: ["a", "b"]`) and a single inline value; surrounding quotes are stripped, blank and `#` comment lines are ignored. The key is inert for rule loading — `rule-loader.mjs`'s `parseGlobsFrontmatter` skips unknown top-level keys and their continuation lines. A declared pattern is reported in `notes[]` with its file, the pattern, whether it matches `≥1` or `0` tracked files here, and the declaration `source` (`frontmatter`). The grain is per pattern AND per rule file: declaring one glob amnesties neither a sibling glob in the same rule nor the same pattern in another rule. There is deliberately NO checker-side exemption list: a rule that does not declare the pattern itself is not exempt, so the declaration always sits with the rule it describes.
69
71
 
70
72
  Check 10 (`docs-parity`, issue #780) validates the public docs surface against live repository state via three sub-checks, all reported under the single `docs-parity` check id: **(a) count-claims** — `docs/components.md`'s own heading counts ("## Skills (N user-facing)", "## Commands (N)", "## Agents (N typed sub-agents)", "## Hook event types (N)") are compared against the SAME actual on-disk derivation the surface-count family (Check 5) uses (`countSkills`/the commands-dir listing/`countAgents`/`readHookCounts`) — NOT Check 5's `claimRe` regexes, which are tuned for CLAUDE.md/README prose phrasing and verifiably do not match `components.md`'s own heading convention; a dedicated set of regexes tailored to the doc's actual authored structure is used instead. **(b) config-block-parity** — top-level Session Config keys documented in `docs/session-config-template.md` (opt-in baseline, via the Check 6 'last'-occurrence extractor) are compared against `docs/session-config-reference.md`; a key is considered documented in the reference when it appears in a `yaml` fence (any indent), a `##`/`###`/`####` heading, or the first cell of a markdown table row (`` | `key-name` | ... ``) — the reference's dominant documentation shape is one table row per field, so a naive fence-only extraction under-counts what it actually documents. Missing keys surface as `docs-parity` errors attributed to `docs/session-config-reference.md`. **(c) metrics-path-liveness** — any `.claude/metrics/` path reference (stale; canonical is `.orchestrator/metrics/`) found in root `docs/*.md` or `docs/examples/*.md` surfaces as an error at its exact file/line. The whole check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `docs/components.md` is absent, or explicitly via `--skip-docs-parity`.
71
73
 
@@ -124,10 +126,15 @@ Environment:
124
126
  "warnings": [
125
127
  { "check": "<name>", "file": "<relative path>", "line": N, "message": "<human>", "extracted": "<raw text>" }
126
128
  ],
129
+ "notes": [
130
+ { "check": "rule-scoping", "probe": "fleet-intent-glob", "file": "<relative path>", "line": N, "message": "<human>", "extracted": "<pattern>", "source": "frontmatter" }
131
+ ],
127
132
  "command_count": { "actual": N }
128
133
  }
129
134
  ```
130
135
 
136
+ `notes[]` is the THIRD output category, alongside `errors[]` and `warnings[]`: a finding that is reported but asks for no action. It never affects `status` or the exit code, and it is always present (empty array when nothing was noted, including on the short-circuit "no scope files matched" path). Its only producer today is Check 9's fleet-intent-glob probe.
137
+
131
138
  The `resolved_path` / `resolved_kind` pair surfaces the alias resolution outcome (issue #33 AC2) so users on either platform can audit which instruction file the checker scanned. `kind: 'claude'` for `CLAUDE.md`, `kind: 'agents'` for `AGENTS.md`, `null` when neither was found.
132
139
 
133
140
  When `command-count` fires a drift error, the error object also carries `"command_count": { "actual": N, "claimed": M }` for easy programmatic diffing.
@@ -551,6 +551,21 @@ function lookupIssueState(iid, repo, cache, vaultDir) {
551
551
  // of being silently skipped, since a completeness audit that silently
552
552
  // drops files defeats its purpose. WARN, not error — an unreadable file
553
553
  // must not brick the gate under `mode: hard`.
554
+ // 6. fleet-intent-glob → notes[]: a glob DECLARED as fleet intent in the
555
+ // rule's own `fleet-intent-globs:` frontmatter key — reported with its
556
+ // local match count, never warned, and it settles probes 3+4 for that
557
+ // pattern. See FLEET_INTENT_FM_KEY below.
558
+ //
559
+ // This list is numeric, not a call sequence — the code below does NOT run
560
+ // 1→2→3→4→5→6 in order. Per rule file: probe 5's read-failure guard runs
561
+ // FIRST (it gates every other probe via `continue` on an unreadable file,
562
+ // before probe 1 ever sees the content), then probe 1, then probes 3/4 with
563
+ // probe 6 interleaved INSIDE the same per-pattern loop (it must intercept a
564
+ // declared fleet-intent pattern before 3/4 evaluate it), then probe 2b: once
565
+ // every rule file has been visited, probe 2a runs last over CLAUDE.md/
566
+ // AGENTS.md. `grep -n "Probe [0-9]" checker.mjs` therefore surfaces markers
567
+ // in code order (5, 1, 6, 2b, 2a), not numeric order — that is intentional,
568
+ // not drift; do not "fix" it by moving probe 6's marker next to probe 5's.
554
569
  // ───────────────────────────────────────────────────────────────────────────
555
570
 
556
571
  let _picomatchRuleScoping = null;
@@ -642,6 +657,95 @@ function extractFrontmatterBlockBody(content) {
642
657
  * foreign-glob probe's discriminator, per Check 9 spec. */
643
658
  const FOREIGN_GLOB_TOKEN_RE = /[A-Z][a-z]+[A-Z]/;
644
659
 
660
+ // ───────────────────────────────────────────────────────────────────────────
661
+ // Fleet-intent globs (Probe 6) — an exemption for globs that are DELIBERATELY
662
+ // zero-match in THIS repo because the rule ships to consumer repos with a
663
+ // different language convention.
664
+ //
665
+ // Why this exists: `.claude/rules/testing.md` carries `**/*Tests*` — the
666
+ // Java/C#/Swift test-naming convention. This repo is pure `.mjs`
667
+ // (`*.test.mjs`), so the pattern matches 0 tracked files here and the
668
+ // zero-match probe warned on it on every run. On 2026-09-09 an agent acted on
669
+ // exactly that warning and DELETED the glob; `tests/skills/config-reading-glob-rules.test.mjs`
670
+ // (which pins it as fleet intent, #445) went red and the deletion was reverted
671
+ // coordinator-direct. A warning whose only available action is "click away"
672
+ // trains clicking away — and here it had already caused the removal of the
673
+ // thing it was meant to protect (`.claude/rules/host-resources.md` § HR-101).
674
+ //
675
+ // The fix is CATEGORY SEPARATION, not suppression (`development.md` § Guard &
676
+ // Threshold Design): a declared fleet-intent glob is REPORTED in `notes[]`
677
+ // ("0 local matches, declared fleet intent — in order") instead of being
678
+ // warned about. Nothing is silenced; the finding simply stops asking for an
679
+ // action that does not exist.
680
+ //
681
+ // Declaration lives WITH the rule, as a frontmatter list:
682
+ //
683
+ // ---
684
+ // globs:
685
+ // - "**/*Tests*"
686
+ // fleet-intent-globs:
687
+ // - "**/*Tests*" # Java/C#/Swift convention — matches in consumer repos
688
+ // ---
689
+ //
690
+ // rule-loader.mjs's parseGlobsFrontmatter ignores unknown top-level keys (and
691
+ // skips their indented continuation lines), so the key is inert for rule
692
+ // loading. The declaration is per-pattern on purpose: marking one glob must
693
+ // never amnesty a genuinely dead sibling glob in the same rule.
694
+ const FLEET_INTENT_FM_KEY = 'fleet-intent-globs';
695
+
696
+ function stripYamlQuotes(s) {
697
+ return s.replace(/^["']|["']$/g, '');
698
+ }
699
+
700
+ /**
701
+ * Parses the `fleet-intent-globs:` frontmatter key out of a raw frontmatter
702
+ * block body. Accepts block style (`key:` + indented ` - value` lines),
703
+ * flow style (`key: ["a", "b"]`) and a single inline value.
704
+ *
705
+ * @param {string|null} fmBody - output of extractFrontmatterBlockBody()
706
+ * @returns {string[]} declared patterns (possibly empty)
707
+ */
708
+ function parseFleetIntentGlobs(fmBody) {
709
+ if (!fmBody) return [];
710
+ const out = [];
711
+ let inBlock = false;
712
+ for (const raw of fmBody.split(/\r?\n/)) {
713
+ const line = raw.replace(/\s+$/, '');
714
+ if (line === '' || /^\s*#/.test(line)) continue;
715
+ if (inBlock) {
716
+ const seq = line.match(/^\s+-\s+(.*)$/);
717
+ if (seq) { out.push(stripYamlQuotes(seq[1].trim())); continue; }
718
+ inBlock = false;
719
+ }
720
+ if (/^\s/.test(line)) continue; // another block's continuation
721
+ const colonIdx = line.indexOf(':');
722
+ if (colonIdx === -1) continue;
723
+ if (line.slice(0, colonIdx).trim() !== FLEET_INTENT_FM_KEY) continue;
724
+ const value = line.slice(colonIdx + 1).trim();
725
+ if (value === '') { inBlock = true; continue; }
726
+ if (value.startsWith('[') && value.endsWith(']')) {
727
+ const inner = value.slice(1, -1).trim();
728
+ if (inner) out.push(...inner.split(',').map((s) => stripYamlQuotes(s.trim())));
729
+ continue;
730
+ }
731
+ out.push(stripYamlQuotes(value));
732
+ }
733
+ return out.filter(Boolean);
734
+ }
735
+
736
+ /**
737
+ * Resolves whether `pattern` is a declared fleet-intent glob. The rule file's
738
+ * own `fleet-intent-globs:` frontmatter is the ONLY source — knowledge about a
739
+ * rule stays with the rule. (The one-entry built-in migration seed for the
740
+ * #445 `testing.md` / `**\/*Tests*` case was removed once that file declared
741
+ * the key itself; no checker-side exemption list exists any more.)
742
+ *
743
+ * @returns {'frontmatter'|null} declaration source, or null
744
+ */
745
+ function fleetIntentSource(pattern, declaredSet) {
746
+ return declaredSet.has(pattern) ? 'frontmatter' : null;
747
+ }
748
+
645
749
  /**
646
750
  * Extracts bare `<name>.md` tokens from a "## See Also" footer's body lines,
647
751
  * skipping any token that carries a path separator (cross-directory
@@ -819,30 +923,60 @@ function main() {
819
923
 
820
924
  const errors = [];
821
925
  const warnings = [];
926
+ // Informational findings — never block, never count as a warning. Currently
927
+ // fed only by rule-scoping's fleet-intent-glob probe (see FLEET_INTENT_FM_KEY
928
+ // above). Additive JSON key: consumers reading `errors`/`warnings` are
929
+ // unaffected.
930
+ const notes = [];
822
931
  const issueCache = new Map();
823
932
 
824
933
  // Check 6: session-config-parity (issue #30) — diff top-level keys under
825
934
  // `## Session Config` between the canonical template and the local
826
935
  // instruction file.
827
936
  //
828
- // Severity split (issue #785 follow-up coordinator triage of the initial
829
- // #785 fix): a missing MANDATORY key (present in the template's "Full
830
- // minimal baseline" — the 7 schema-enforced keys) is an ERROR. A missing
831
- // OPT-IN key (present only in the "Full opt-in baseline" — i.e. NOT in the
832
- // minimal block) is a WARNING, not an error: a consumer repo that
833
- // legitimately does not adopt an opt-in feature (e.g. no `handover-gate`)
834
- // must not go red — this repo's own CLAUDE.md deliberately omits 37
835
- // opt-in-baseline keys. `mode: hard` only exits non-zero on `errors[]`, so
836
- // pure opt-in gaps never block `autonomous-gated` skill-evolution's
837
- // `runConfigValidationGate()`.
937
+ // Three-category split (issue #30 #785 #1356):
938
+ // · missing MANDATORY key (present in the template's "Full minimal
939
+ // baseline" — the schema-enforced keys) errors[] (unchanged)
940
+ // · missing OPT-IN key (present only in the "Full opt-in baseline")
941
+ // notes[] (#1356)
942
+ // · local key UNKNOWN to the template union → warnings[] (#1356)
943
+ //
944
+ // #785's follow-up put the opt-in gap in `warnings[]`. Measured 2026-09-13
945
+ // @ ff1ed191 that produced 36 of this repo's 36 config-parity warnings — a
946
+ // whole warning class firing on every single run, each entry saying "not
947
+ // required" and offering no action. `.claude/rules/host-resources.md`
948
+ // § HR-101 calls that a broken instrument; the repair is CATEGORY
949
+ // SEPARATION, never suppression and never a threshold (`development.md`
950
+ // § Guard & Threshold Design), exactly as the fleet-intent-glob probe did
951
+ // for rule-scoping (#1312). So a deliberately-unadopted opt-in feature is
952
+ // now REPORTED in `notes[]` — still emitted, still rendered (session-end's
953
+ // drift-operations.md renders `notes[]` under its own heading), but no
954
+ // longer asking for an action that does not exist.
955
+ //
956
+ // What replaces it in `warnings[]` is the direction nothing inspected
957
+ // before: a top-level key in the LOCAL Session Config that the template's
958
+ // `## Session Config` blocks do not carry. That one IS actionable — it is
959
+ // either a typo/rename, or the template's baseline blocks have not caught
960
+ // up, and both are fixed by an edit.
838
961
  //
839
- // This is a deliberate, coordinator-directed DEVIATION from #785's literal
840
- // fix-direction ("plants an opt-in key... asserts a session-config-parity
841
- // ERROR") the mechanism (union template keys via `{ occurrence: 'last' }`)
842
- // is unchanged, but the missing-opt-in-key case now lands in `warnings[]`
843
- // instead of `errors[]`. Local keys unknown to the template union were
844
- // (and remain) never flagged in either direction — no existing code path
845
- // inspected that direction before this change either.
962
+ // NAME THE POPULATION (fix-pass 2026-09-13). This check reads exactly ONE
963
+ // population: the template's `## Session Config` blocks. It therefore
964
+ // CANNOT know whether the key is documented elsewhere in the template, nor
965
+ // whether anything reads it at runtime and #1356's first wording asserted
966
+ // both. Measured 2026-09-13 @ ff1ed191 in this repo, it fired on three keys
967
+ // and was wrong about two of them:
968
+ // awk '/^## /{sec=$0} /^auto-skill-dispatch:|^issue-budget:/{print NR,sec}' \
969
+ // docs/session-config-template.md
970
+ // → auto-skill-dispatch line 87 (## Auto-Skill Dispatch)
971
+ // → issue-budget line 123 (## Issue Budget)
972
+ // rg -c "'auto-skill-dispatch'" scripts/ → 4 files, non-zero
973
+ // rg -c "'issue-budget'" scripts/ → non-zero
974
+ // Both are documented in the template FILE and both are read at runtime;
975
+ // only their absence from the two `## Session Config` blocks (lines 658/676)
976
+ // was ever true. The message below now asserts only that.
977
+ //
978
+ // `mode: hard`/`strict` still keys off `errors[]` only, so neither notes
979
+ // nor the new warnings can block `runConfigValidationGate()`.
846
980
  let configParityRan = false;
847
981
  if (!args.skipSessionConfigParity) {
848
982
  const templatePath = args.configTemplate
@@ -887,9 +1021,26 @@ function main() {
887
1021
  extracted: key,
888
1022
  });
889
1023
  } else {
1024
+ notes.push({
1025
+ check: 'session-config-parity', probe: 'opt-in-gap', file: rel, line,
1026
+ message: `Session Config omits opt-in top-level key '${key}' (documented in docs/session-config-template.md's opt-in baseline; not required). Reported, not warned.`,
1027
+ extracted: key,
1028
+ });
1029
+ }
1030
+ }
1031
+ // Actionable direction (#1356): a local top-level key the template
1032
+ // union never documents. Only reachable when the template HAS an
1033
+ // opt-in block distinct from the minimal one — a single-block
1034
+ // template (test fixture, or a repo that never split the baselines)
1035
+ // is not a key catalog, so diffing against it would warn on every
1036
+ // legitimately-adopted key.
1037
+ if (tplMinimalBlock && tplBlock.headingLine !== tplMinimalBlock.headingLine) {
1038
+ const tplKeySet = new Set(tplKeys);
1039
+ for (const key of localKeys) {
1040
+ if (tplKeySet.has(key)) continue;
890
1041
  warnings.push({
891
1042
  check: 'session-config-parity', file: rel, line,
892
- message: `Session Config omits opt-in top-level key '${key}' (documented in docs/session-config-template.md's opt-in baseline; not required)`,
1043
+ message: `Session Config declares top-level key '${key}' which is absent from both '## Session Config' blocks of docs/session-config-template.md (the only population this check reads) — a typo/rename, or template baseline blocks that have not caught up. This check does not inspect the rest of the template file or any runtime reader, so the key may well be documented elsewhere and in use`,
893
1044
  extracted: key,
894
1045
  });
895
1046
  }
@@ -1127,7 +1278,7 @@ function main() {
1127
1278
 
1128
1279
  // Check 9: rule-scoping — validates .claude/rules/*.md frontmatter against
1129
1280
  // the rule-loader.mjs contract (see the doc-comment above the helper
1130
- // functions for the five probes, incl. unreadable-file). Silently skipped (no id pushed, no
1281
+ // functions for the six probes, incl. unreadable-file and fleet-intent-glob). Silently skipped (no id pushed, no
1131
1282
  // checksSkipped entry) when .claude/rules/ is absent — mirrors Check 8's
1132
1283
  // silent-skip semantics. `--skip-rule-scoping` disables the whole check.
1133
1284
  if (!args.skipRuleScoping) {
@@ -1149,6 +1300,11 @@ function main() {
1149
1300
  const absPath = join(rulesDir, fname);
1150
1301
  const relPath = relative(vaultDir, absPath);
1151
1302
  let content;
1303
+ // --- Probe 5: unreadable-file → warnings[] ---
1304
+ // Runs BEFORE probe 1 despite its higher number: it is the per-file
1305
+ // guard that must decide whether any other probe gets to see this
1306
+ // file's content at all. See the "Rule-scoping family" header
1307
+ // comment above for why the numbering and the code order differ.
1152
1308
  try {
1153
1309
  content = readFileSync(absPath, 'utf8');
1154
1310
  } catch (err) {
@@ -1190,7 +1346,25 @@ function main() {
1190
1346
  const globs = parsed.globs;
1191
1347
  if (Array.isArray(globs) && globs.length > 0) {
1192
1348
  if (trackedFiles === null) trackedFiles = listTrackedFiles(vaultDir);
1349
+ const declaredFleetIntent = new Set(parseFleetIntentGlobs(fmBody));
1193
1350
  for (const pattern of globs) {
1351
+ // --- Probe 6: fleet-intent-glob → notes[] ---
1352
+ // A DECLARED fleet-intent glob is reported, never warned: it is
1353
+ // zero-match here on purpose and serves consumer repos. The
1354
+ // declaration also settles the foreign-glob probe for the same
1355
+ // pattern — a pattern the rule author declared is by definition
1356
+ // not a copy-paste leftover.
1357
+ const fleetSource = fleetIntentSource(pattern, declaredFleetIntent);
1358
+ if (fleetSource !== null) {
1359
+ const matchesLocally = globMatchesAny(pattern, trackedFiles);
1360
+ notes.push({
1361
+ check: 'rule-scoping', probe: 'fleet-intent-glob', file: relPath, line: 1,
1362
+ message: `glob '${pattern}' matches ${matchesLocally ? '≥1' : '0'} tracked files here and is declared FLEET INTENT (${fleetSource}) — it serves consumer repos with a different language convention. Reported, not warned.`,
1363
+ extracted: pattern,
1364
+ source: fleetSource,
1365
+ });
1366
+ continue;
1367
+ }
1194
1368
  if (!globMatchesAny(pattern, trackedFiles)) {
1195
1369
  warnings.push({
1196
1370
  check: 'rule-scoping', file: relPath, line: 1,
@@ -1415,7 +1589,7 @@ function main() {
1415
1589
  status: 'skipped', mode: args.mode, vault_dir: vaultDir,
1416
1590
  resolved_path: resolvedPath, resolved_kind: resolvedKind,
1417
1591
  files_scanned: 0, checks_run: checksRun, checks_skipped: checksSkipped,
1418
- errors, warnings, reason: 'no scope files matched',
1592
+ errors, warnings, notes, reason: 'no scope files matched',
1419
1593
  }) + '\n');
1420
1594
  process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);
1421
1595
  }
@@ -1572,11 +1746,29 @@ function main() {
1572
1746
  files_scanned: scopeFiles.length,
1573
1747
  checks_run: checksRun,
1574
1748
  checks_skipped: checksSkipped,
1575
- errors, warnings,
1749
+ errors, warnings, notes,
1576
1750
  };
1577
1751
  if (actualCommandCount !== null) {
1578
1752
  result.command_count = { actual: actualCommandCount };
1579
1753
  }
1754
+ // --- notes[] human renderer (#1312) ---
1755
+ // `notes[]` is the REPORT-ONLY category the fleet-intent split created
1756
+ // (.claude/rules/development.md § Guard & Threshold Design: separate the
1757
+ // category, never raise the threshold). Built and returned in the JSON since
1758
+ // that split, it had no renderer — so a note reached no human reader, and an
1759
+ // unread category is the state the split replaced, not a weaker warning.
1760
+ // stdout stays pure JSON (the consumer contract); notes leave on stderr as
1761
+ // diagnostics, under their own heading, and never carry the word "warning"
1762
+ // or "error" — that wording IS the category separation at the output edge.
1763
+ if (notes.length > 0) {
1764
+ process.stderr.write(
1765
+ `NOTES (${notes.length}) \u2014 reported, not warned; no action implied:\n`
1766
+ + notes
1767
+ .map((n) => ` \u00b7 [${n.check}/${n.probe}] ${n.file}:${n.line} \u2014 ${n.message}\n`)
1768
+ .join(''),
1769
+ );
1770
+ }
1771
+
1580
1772
  process.stdout.write(JSON.stringify(result) + '\n');
1581
1773
 
1582
1774
  process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);