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.
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +94 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/commands/ux-grill.md +51 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/codex-setup.md +8 -0
- package/docs/components.md +7 -7
- package/docs/events-schema.md +9 -5
- package/docs/github-mirror-protection.md +61 -20
- package/docs/migration-v5.md +62 -0
- package/docs/scope-collision-guard.md +14 -0
- package/hooks/_lib/hook-import-set.json +44 -2
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +190 -3
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +51 -4
- package/package.json +2 -1
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +114 -13
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget.mjs +336 -6
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +11 -1
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +14 -65
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-start-probes.mjs +419 -53
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +48 -20
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/mcp-server.sh +16 -1
- package/scripts/release.mjs +7 -2
- package/skills/bootstrap/SKILL.md +12 -209
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +7 -3
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +10 -0
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +4 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-plan/SKILL.md +2 -144
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-start/SKILL.md +24 -6
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +3 -128
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +18 -5
- package/templates/_shared/ux-manifest.template.md +149 -0
- 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` |
|
|
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.
|
|
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
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
// OPT-IN key (present only in the "Full opt-in baseline"
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
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
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
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
|
|
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
|
|
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);
|