session-orchestrator 5.3.0 → 5.4.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/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +70 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +57 -4
- package/docs/components.md +12 -12
- package/docs/events-schema.md +11 -8
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +55 -6
- package/docs/session-config-template.md +11 -2
- package/hooks/_lib/hook-import-set.json +27 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +53 -0
- package/hooks/on-stop.mjs +3 -1
- package/hooks/post-tool-batch-wave-signal.mjs +3 -1
- package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +559 -72
- package/hooks/subagent-telemetry.mjs +96 -19
- package/package.json +3 -3
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +49 -53
- package/scripts/backfill-abandoned-sessions.mjs +1 -1
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/emit-session.mjs +296 -2
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +10 -6
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +71 -10
- package/scripts/lib/events.mjs +21 -2
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/instruction-budget-guard.mjs +69 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +43 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +62 -11
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +10 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +323 -302
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +72 -46
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/evolve/SKILL.md +2 -2
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +22 -60
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -20,16 +20,17 @@
|
|
|
20
20
|
* `globs:`. Precedence when BOTH keys are present on the same rule: `globs:`
|
|
21
21
|
* wins (silently — no merge, no warning) and `paths:` is ignored entirely.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
23
|
+
* Frontmatter counts ONLY when its opening `---` is the very first line of the
|
|
24
|
+
* file (#1449) — the same rule Claude Code applies when it reads `paths:`.
|
|
25
|
+
* Until #1449 this parser skipped leading blank lines and single-line HTML
|
|
26
|
+
* comments (the vendoring pipeline's `<!-- source: … -->` provenance header),
|
|
27
|
+
* so a rule Claude Code loads ALWAYS-ON was classified path-scoped here and
|
|
28
|
+
* `instruction-budget-guard.mjs` under-counted the always-on surface. A
|
|
29
|
+
* provenance comment now belongs directly AFTER the closing `---` (body text,
|
|
30
|
+
* inert). A frontmatter-shaped block that is NOT on line 1 — any text,
|
|
31
|
+
* comment or blank line before it — yields no globs and no meta at all, and
|
|
32
|
+
* the entry carries `defect: 'frontmatter-not-at-top'` so callers can report
|
|
33
|
+
* the file instead of silently loading it always-on.
|
|
33
34
|
*
|
|
34
35
|
* Beyond the `globs:` key (issue #336), the frontmatter parser also captures
|
|
35
36
|
* these scalar activation keys (issue #694), surfaced on each RuleEntry:
|
|
@@ -150,34 +151,15 @@ function matchGlob(filePath, globPattern) {
|
|
|
150
151
|
// Minimal YAML frontmatter parser (matches state-md.mjs style)
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
153
|
|
|
154
|
+
// Anchored at offset 0: frontmatter exists only when `---` is line 1 (#1449).
|
|
153
155
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
154
156
|
|
|
155
|
-
// A single header line is either blank or a complete single-line HTML
|
|
156
|
-
// comment (`<!-- ... -->` on one line). Multi-line comment blocks are
|
|
157
|
-
// deliberately NOT supported — the vendoring convention is a single first
|
|
158
|
-
// line, and tolerating a few stacked comment lines (each matching this RE)
|
|
159
|
-
// is sufficient.
|
|
160
|
-
const HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
|
|
161
|
-
|
|
162
157
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* mandatory provenance header precedes it. Stops at the first line that is
|
|
167
|
-
* neither blank nor a complete single-line comment.
|
|
168
|
-
*
|
|
169
|
-
* @param {string} contents - raw file contents
|
|
170
|
-
* @returns {string} contents starting at the first non-header line (empty
|
|
171
|
-
* string when the entire content is header lines)
|
|
158
|
+
* Defect code for a frontmatter block that does not start on line 1 (#1449).
|
|
159
|
+
* Claude Code ignores such a block, so the rule loads always-on there; the
|
|
160
|
+
* loader mirrors that and tags the entry with this code.
|
|
172
161
|
*/
|
|
173
|
-
|
|
174
|
-
const lines = contents.split(/\r?\n/);
|
|
175
|
-
let idx = 0;
|
|
176
|
-
while (idx < lines.length && HEADER_LINE_RE.test(lines[idx])) {
|
|
177
|
-
idx++;
|
|
178
|
-
}
|
|
179
|
-
return lines.slice(idx).join('\n');
|
|
180
|
-
}
|
|
162
|
+
export const FRONTMATTER_NOT_AT_TOP = 'frontmatter-not-at-top';
|
|
181
163
|
|
|
182
164
|
// Scalar activation keys captured (issue #694 + #692). The `globs` key keeps
|
|
183
165
|
// its dedicated sequence/flow handling and is NOT routed through this table.
|
|
@@ -193,12 +175,36 @@ const SCALAR_META_KEYS = new Set([
|
|
|
193
175
|
'tier',
|
|
194
176
|
]);
|
|
195
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
|
|
180
|
+
*
|
|
181
|
+
* `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
|
|
182
|
+
* js-yaml, which picks SINGLE-quote style whenever the plain form would be
|
|
183
|
+
* ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
|
|
184
|
+
* leading/trailing-quote strip then returns `it''s` for `it's` — measured
|
|
185
|
+
* 2026-08-23 on 40 of 146 live learning insights carrying both characters.
|
|
186
|
+
* Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
|
|
187
|
+
* `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
|
|
188
|
+
*/
|
|
189
|
+
function unquoteYamlScalar(valuePart) {
|
|
190
|
+
const v = valuePart.trim();
|
|
191
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
192
|
+
return v.slice(1, -1).replace(/''/g, "'");
|
|
193
|
+
}
|
|
194
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
195
|
+
return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
|
|
196
|
+
}
|
|
197
|
+
return v.replace(/^["']|["']$/g, '');
|
|
198
|
+
}
|
|
199
|
+
|
|
196
200
|
/**
|
|
197
201
|
* Parses the YAML frontmatter block for the `globs:` field (issue #336),
|
|
198
202
|
* its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
|
|
199
203
|
*
|
|
200
204
|
* Returns:
|
|
201
205
|
* - `{ globs: string[] | null, meta: object }` on success
|
|
206
|
+
* - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
|
|
207
|
+
* frontmatter block exists but does not start on line 1 (#1449)
|
|
202
208
|
* - throws `Error` on malformed frontmatter so the caller can fall back
|
|
203
209
|
*
|
|
204
210
|
* `globs` is `null` when no frontmatter, and neither `globs:` nor `paths:` is
|
|
@@ -213,41 +219,63 @@ const SCALAR_META_KEYS = new Set([
|
|
|
213
219
|
* - all other recognised keys → quote-stripped strings
|
|
214
220
|
* Unknown keys are ignored without error.
|
|
215
221
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
222
|
+
* Only a block whose opening `---` is line 1 is frontmatter (#1449), exactly
|
|
223
|
+
* as Claude Code reads it. Anything before the opener — a provenance comment,
|
|
224
|
+
* a blank line, any text — makes the block body text: no globs, no meta, and
|
|
225
|
+
* the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
|
|
220
226
|
*
|
|
221
227
|
* @param {string} contents - raw file contents
|
|
222
|
-
* @returns {{ globs: string[] | null, meta: Record<string, unknown
|
|
228
|
+
* @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
|
|
223
229
|
*/
|
|
230
|
+
export function parseGlobsFrontmatter(contents) {
|
|
231
|
+
const match = FRONTMATTER_RE.exec(contents);
|
|
232
|
+
if (match) return parseFrontmatterBlock(match[1]);
|
|
233
|
+
if (hasDisplacedFrontmatter(contents)) {
|
|
234
|
+
return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
|
|
235
|
+
}
|
|
236
|
+
return { globs: null, meta: {} };
|
|
237
|
+
}
|
|
238
|
+
|
|
224
239
|
/**
|
|
225
|
-
*
|
|
240
|
+
* Does `contents` carry a frontmatter block that is NOT on line 1 (#1449)?
|
|
226
241
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
242
|
+
* Only the FIRST bare `---` line is considered, and its block must parse
|
|
243
|
+
* cleanly AND carry at least one key this loader honours (`globs`, `paths`,
|
|
244
|
+
* or a scalar activation key). That second condition keeps a horizontal rule
|
|
245
|
+
* pair around ordinary prose from being reported: prose between two `---`
|
|
246
|
+
* lines almost always holds a colon-less line (a heading, a sentence), which
|
|
247
|
+
* throws, and a recognised key at column 0 is what makes the misplacement
|
|
248
|
+
* change behaviour at all. The known blind spot is a displaced block that is
|
|
249
|
+
* itself malformed — it is treated as body text, which is what Claude Code
|
|
250
|
+
* does with it too.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} contents - raw file contents
|
|
253
|
+
* @returns {boolean}
|
|
234
254
|
*/
|
|
235
|
-
function
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
255
|
+
function hasDisplacedFrontmatter(contents) {
|
|
256
|
+
const lines = contents.split(/\r?\n/);
|
|
257
|
+
const opener = lines.indexOf('---');
|
|
258
|
+
// -1: no opener at all. 0: the opener IS line 1, so FRONTMATTER_RE already
|
|
259
|
+
// had its chance (an unclosed block) — not a displacement.
|
|
260
|
+
if (opener <= 0) return false;
|
|
261
|
+
const block = FRONTMATTER_RE.exec(lines.slice(opener).join('\n'));
|
|
262
|
+
if (!block) return false;
|
|
263
|
+
try {
|
|
264
|
+
const { globs, meta } = parseFrontmatterBlock(block[1]);
|
|
265
|
+
return globs !== null || Object.keys(meta).length > 0;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
242
268
|
}
|
|
243
|
-
return v.replace(/^["']|["']$/g, '');
|
|
244
269
|
}
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Parses the text BETWEEN the two `---` fences (the frontmatter body).
|
|
273
|
+
* Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} fmText - frontmatter body without its fences
|
|
276
|
+
* @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
|
|
277
|
+
*/
|
|
278
|
+
function parseFrontmatterBlock(fmText) {
|
|
251
279
|
const lines = fmText.split(/\r?\n/);
|
|
252
280
|
|
|
253
281
|
let globsValue = null;
|
|
@@ -466,6 +494,9 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
|
466
494
|
* from the `alwaysApply` frontmatter key)
|
|
467
495
|
* @property {string[]} matchedGlobs - globs that matched; empty when alwaysOn
|
|
468
496
|
* @property {true} [_parseError] - present only on frontmatter parse-error entries
|
|
497
|
+
* @property {'frontmatter-not-at-top'} [defect] - present only when a frontmatter
|
|
498
|
+
* block exists but does not start on line 1 (#1449); such an entry is always
|
|
499
|
+
* `alwaysOn: true` and carries no meta fields, mirroring Claude Code
|
|
469
500
|
* @property {string} [description] - frontmatter `description` (issue #694), when present
|
|
470
501
|
* @property {string} [mode] - frontmatter `mode` (issue #694), when present
|
|
471
502
|
* @property {string} [hostClass] - frontmatter `host-class` (issue #694), when present
|
|
@@ -484,6 +515,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
|
484
515
|
* Rules without `globs:` (or its `paths:` alias — issue #795) frontmatter are
|
|
485
516
|
* always included (alwaysOn: true). Rules with `globs:`/`paths:` are included
|
|
486
517
|
* only when at least one scopePath matches at least one glob pattern.
|
|
518
|
+
* A rule whose frontmatter does not start on line 1 is always-on with no meta
|
|
519
|
+
* (so no gate excludes it) and carries `defect: 'frontmatter-not-at-top'`
|
|
520
|
+
* (#1449) — no stderr line, because callers such as the instruction-budget
|
|
521
|
+
* guard load the directory several times per run and report the defect themselves.
|
|
487
522
|
*
|
|
488
523
|
* After a successful frontmatter parse, deterministic gates (issue #694) are
|
|
489
524
|
* applied to BOTH always-on and glob-matched rules — a rule must pass ALL
|
|
@@ -557,10 +592,11 @@ export function loadApplicableRules({
|
|
|
557
592
|
|
|
558
593
|
let globs;
|
|
559
594
|
let meta;
|
|
595
|
+
let defect;
|
|
560
596
|
let parseError = false;
|
|
561
597
|
|
|
562
598
|
try {
|
|
563
|
-
({ globs, meta } = parseGlobsFrontmatter(content));
|
|
599
|
+
({ globs, meta, defect } = parseGlobsFrontmatter(content));
|
|
564
600
|
} catch (err) {
|
|
565
601
|
process.stderr.write(
|
|
566
602
|
`[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,
|
|
@@ -581,7 +617,8 @@ export function loadApplicableRules({
|
|
|
581
617
|
const metaFields = metaToEntryFields(meta);
|
|
582
618
|
|
|
583
619
|
if (globs === null) {
|
|
584
|
-
// No globs frontmatter (or parse error
|
|
620
|
+
// No globs frontmatter (or parse error, or frontmatter not on line 1 —
|
|
621
|
+
// #1449) → always-on
|
|
585
622
|
results.push({
|
|
586
623
|
path: filePath,
|
|
587
624
|
content,
|
|
@@ -589,6 +626,7 @@ export function loadApplicableRules({
|
|
|
589
626
|
matchedGlobs: [],
|
|
590
627
|
...metaFields,
|
|
591
628
|
...(parseError ? { _parseError: true } : {}),
|
|
629
|
+
...(defect ? { defect } : {}),
|
|
592
630
|
});
|
|
593
631
|
continue;
|
|
594
632
|
}
|
|
@@ -3,26 +3,37 @@
|
|
|
3
3
|
* Syncs canonical rules from the plugin's rules/ library into a consumer repo's .claude/rules/.
|
|
4
4
|
*
|
|
5
5
|
* Stdlib-only, cross-platform, no Zod, no third-party parsers. ESM module.
|
|
6
|
-
* Preserves local rules
|
|
6
|
+
* Preserves local rules — files carrying the plugin provenance header in
|
|
7
|
+
* neither accepted position (see `isPluginOwnedContent`).
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
11
|
import { join, basename, dirname, resolve } from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
validateRuleContent,
|
|
15
|
+
scanVendoringLeaks,
|
|
16
|
+
isPluginOwnedContent,
|
|
17
|
+
} from './validate-vendored-rules.mjs';
|
|
13
18
|
import { isMainModule } from './is-main-module.mjs';
|
|
14
19
|
|
|
15
20
|
// The vendoring sanitizer (issue #1098) lives in validate-vendored-rules.mjs —
|
|
16
21
|
// it has that module's shape ("judge one rule file → findings") and its
|
|
17
22
|
// standalone CLI needs it too. Re-exported here so importers that predate the
|
|
18
23
|
// move (and this file's own tests) keep resolving it from rules-sync.mjs.
|
|
19
|
-
|
|
24
|
+
//
|
|
25
|
+
// isPluginOwnedContent (#1449) — the ownership predicate below — lives there
|
|
26
|
+
// for the same reason as PLUGIN_HEADER_PREFIX's copy: this module imports
|
|
27
|
+
// validate-vendored-rules.mjs, so defining it here and importing it back into
|
|
28
|
+
// the validator's provenance probe would make the two modules import each
|
|
29
|
+
// other. Re-exported so the ownership rule is reachable from the sync module.
|
|
30
|
+
export { scanVendoringLeaks, isPluginOwnedContent };
|
|
20
31
|
|
|
21
32
|
// Exported (issue #722 Epic A Wave 2) for external consumers (e.g. tests).
|
|
22
|
-
// NOT imported by validate-vendored-rules.mjs —
|
|
23
|
-
// validateRuleContent FROM
|
|
24
|
-
// importing this constant back would create a module-load cycle; it
|
|
25
|
-
// its own textually-identical copy instead.
|
|
33
|
+
// NOT imported by validate-vendored-rules.mjs — this module imports
|
|
34
|
+
// validateRuleContent FROM that file (see the pre-write gate below), so
|
|
35
|
+
// importing this constant back there would create a module-load cycle; it
|
|
36
|
+
// keeps its own textually-identical copy instead.
|
|
26
37
|
export const PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin';
|
|
27
38
|
|
|
28
39
|
/**
|
|
@@ -170,8 +181,9 @@ function escapeRegex(s) {
|
|
|
170
181
|
*
|
|
171
182
|
* When `validate` is true (the default — issue #722 Epic A Wave 2),
|
|
172
183
|
* validateRuleContent() runs as a pre-write gate on every source file before
|
|
173
|
-
* it is copied. A file with any error-severity violation (
|
|
174
|
-
* missing provenance header, unfilled placeholder) is
|
|
184
|
+
* it is copied. A file with any error-severity violation (frontmatter-not-at-top,
|
|
185
|
+
* globs-paths-mismatch, missing provenance header, unfilled placeholder) is
|
|
186
|
+
* recorded in `errors[]`
|
|
175
187
|
* and its write is skipped entirely — it is never added to
|
|
176
188
|
* written/skipped/preserved. Warn-severity violations (zero-match-globs,
|
|
177
189
|
* foreign-glob) do NOT block the write; they are collected into the
|
|
@@ -405,9 +417,12 @@ export function syncRules({
|
|
|
405
417
|
continue;
|
|
406
418
|
}
|
|
407
419
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
420
|
+
// Ownership accepts BOTH header positions (#1449): line 1 (the format
|
|
421
|
+
// every copy vendored before #1449 carries, frontmatter or not) and the
|
|
422
|
+
// first non-blank line after a line-1 frontmatter (the current format).
|
|
423
|
+
// So an old-format copy is rewritten into the new format below, never
|
|
424
|
+
// preserved; only a file with no header in either position is local.
|
|
425
|
+
if (!isPluginOwnedContent(targetContent)) {
|
|
411
426
|
preserved.push(fileName);
|
|
412
427
|
continue;
|
|
413
428
|
}
|
|
@@ -604,10 +604,9 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
|
604
604
|
? plannedFilesRaw
|
|
605
605
|
: 0;
|
|
606
606
|
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// edited often enough that a pinned number goes stale, as this very comment did.
|
|
607
|
+
// Division-by-zero guard: a baseline that froze zero planned files (no declared
|
|
608
|
+
// file scope, or a non-numeric count coerced to 0 above) would otherwise yield
|
|
609
|
+
// Infinity or NaN; `max(plannedFiles, 1)` keeps the ratio finite and comparable.
|
|
611
610
|
const filesRatio = round2(actualFiles / Math.max(plannedFiles, 1));
|
|
612
611
|
const breached = filesRatio >= threshold;
|
|
613
612
|
|